mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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
|
||||
banners: 17946
|
||||
mapsEms: 26072
|
||||
timelines: 330000
|
||||
timelines: 327300
|
||||
screenshotMode: 17856
|
||||
visTypePie: 35583
|
||||
expressionRevealImage: 25675
|
||||
|
|
|
@ -21,7 +21,12 @@ import {
|
|||
import type { TimelinesUIStart } from '../../../../timelines/public';
|
||||
import type { TopAlert } from './';
|
||||
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 { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
|
@ -213,6 +218,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
sortDirection: 'desc',
|
||||
},
|
||||
],
|
||||
filterStatus: status as AlertStatus,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
unit: (totalAlerts: number) =>
|
||||
|
|
|
@ -15,7 +15,8 @@ import { inputsModel, inputsSelectors, State } from '../../store';
|
|||
import { inputsActions } from '../../store/actions';
|
||||
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/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 { InspectButtonContainer } from '../inspect';
|
||||
import { useGlobalFullScreen } from '../../containers/use_full_screen';
|
||||
|
@ -54,6 +55,7 @@ export interface OwnProps {
|
|||
showTotalCount?: boolean;
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
pageFilters?: Filter[];
|
||||
currentFilter?: Status;
|
||||
onRuleChange?: () => void;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
|
@ -83,6 +85,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
itemsPerPageOptions,
|
||||
kqlMode,
|
||||
pageFilters,
|
||||
currentFilter,
|
||||
onRuleChange,
|
||||
query,
|
||||
renderCellValue,
|
||||
|
@ -160,6 +163,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
sort,
|
||||
utilityBar,
|
||||
graphEventId,
|
||||
filterStatus: currentFilter,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
})
|
||||
|
|
|
@ -390,6 +390,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
pageFilters={defaultFiltersMemo}
|
||||
defaultModel={defaultTimelineModel}
|
||||
end={to}
|
||||
currentFilter={filterGroup}
|
||||
headerFilterGroup={headerFilterGroup}
|
||||
id={timelineId}
|
||||
onRuleChange={onRuleChange}
|
||||
|
|
|
@ -5,4 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertStatus } from './types/timeline/actions';
|
||||
|
||||
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
|
||||
> &
|
||||
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,
|
||||
tabType: TimelineTabs.query,
|
||||
totalPages: 1,
|
||||
totalItems: 1,
|
||||
leadingControlColumns: [],
|
||||
trailingControlColumns: [],
|
||||
filterStatus: 'open',
|
||||
refetch: jest.fn(),
|
||||
};
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -11,19 +11,34 @@ import {
|
|||
EuiDataGridControlColumn,
|
||||
EuiDataGridStyle,
|
||||
EuiDataGridToolBarVisibilityOptions,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
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 { SortColumnTimeline, TimelineId, TimelineTabs } from '../../../../common/types/timeline';
|
||||
import {
|
||||
TimelineId,
|
||||
TimelineTabs,
|
||||
BulkActionsProp,
|
||||
SortColumnTimeline,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
import type {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
ControlColumnProps,
|
||||
RowRenderer,
|
||||
AlertStatus,
|
||||
} from '../../../../common/types/timeline';
|
||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
|
||||
|
||||
|
@ -32,15 +47,21 @@ import { getEventIdToDataMapping } from './helpers';
|
|||
import { Sort } from './sort';
|
||||
|
||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
|
||||
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import { OnRowSelected, OnSelectAll } from '../types';
|
||||
import { StatefulFieldsBrowser, tGridActions } from '../../../';
|
||||
import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
|
||||
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import type { OnRowSelected, OnSelectAll } from '../types';
|
||||
import type { Refetch } from '../../../store/t_grid/inputs';
|
||||
import { StatefulFieldsBrowser } from '../../../';
|
||||
import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
|
||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||
import { RowAction } from './row_action';
|
||||
import * as i18n from './translations';
|
||||
import { AlertCount } from '../styles';
|
||||
import { checkBoxControlColumn } from './control_columns';
|
||||
|
||||
const StatefulAlertStatusBulkActions = lazy(
|
||||
() => import('../toolbar/bulk_actions/alert_status_bulk_actions')
|
||||
);
|
||||
|
||||
interface OwnProps {
|
||||
activePage: number;
|
||||
additionalControls?: React.ReactNode;
|
||||
|
@ -48,16 +69,22 @@ interface OwnProps {
|
|||
data: TimelineItem[];
|
||||
id: string;
|
||||
isEventViewer?: boolean;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
sort: Sort[];
|
||||
tabType: TimelineTabs;
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
leadingControlColumns?: ControlColumnProps[];
|
||||
trailingControlColumns?: ControlColumnProps[];
|
||||
totalPages: number;
|
||||
totalItems: number;
|
||||
bulkActions?: BulkActionsProp;
|
||||
filterStatus?: AlertStatus;
|
||||
unit?: (total: number) => React.ReactNode;
|
||||
onRuleChange?: () => void;
|
||||
refetch: Refetch;
|
||||
}
|
||||
|
||||
const basicUnit = (n: number) => i18n.UNIT(n);
|
||||
const NUM_OF_ICON_IN_TIMELINE_ROW = 2;
|
||||
|
||||
export const hasAdditionalActions = (id: TimelineId): boolean =>
|
||||
|
@ -200,31 +227,42 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
sort,
|
||||
tabType,
|
||||
totalPages,
|
||||
totalItems,
|
||||
filterStatus,
|
||||
bulkActions = true,
|
||||
unit = basicUnit,
|
||||
leadingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||
trailingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||
refetch,
|
||||
}) => {
|
||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
|
||||
getManageTimeline(state, id)
|
||||
);
|
||||
|
||||
const alertCountText = useMemo(() => `${totalItems.toLocaleString()} ${unit(totalItems)}`, [
|
||||
totalItems,
|
||||
unit,
|
||||
]);
|
||||
|
||||
const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]);
|
||||
|
||||
const onRowSelected: OnRowSelected = useCallback(
|
||||
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
|
||||
setSelected!({
|
||||
setSelected({
|
||||
id,
|
||||
eventIds: getEventIdToDataMapping(data, eventIds, queryFields),
|
||||
isSelected,
|
||||
isSelectAllChecked:
|
||||
isSelected && Object.keys(selectedEventIds).length + 1 === data.length,
|
||||
isSelectAllChecked: isSelected && selectedCount + 1 === data.length,
|
||||
});
|
||||
},
|
||||
[setSelected, id, data, selectedEventIds, queryFields]
|
||||
[setSelected, id, data, selectedCount, queryFields]
|
||||
);
|
||||
|
||||
const onSelectPage: OnSelectAll = useCallback(
|
||||
({ isSelected }: { isSelected: boolean }) =>
|
||||
isSelected
|
||||
? setSelected!({
|
||||
? setSelected({
|
||||
id,
|
||||
eventIds: getEventIdToDataMapping(
|
||||
data,
|
||||
|
@ -234,7 +272,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
isSelected,
|
||||
isSelectAllChecked: isSelected,
|
||||
})
|
||||
: clearSelected!({ id }),
|
||||
: clearSelected({ id }),
|
||||
[setSelected, clearSelected, id, data, queryFields]
|
||||
);
|
||||
|
||||
|
@ -245,25 +283,87 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
}
|
||||
}, [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(
|
||||
() => ({
|
||||
additionalControls: (
|
||||
<>
|
||||
{additionalControls ?? null}
|
||||
{
|
||||
<StatefulFieldsBrowser
|
||||
data-test-subj="field-browser"
|
||||
browserFields={browserFields}
|
||||
timelineId={id}
|
||||
columnHeaders={columnHeaders}
|
||||
/>
|
||||
}
|
||||
<AlertCount>{alertCountText}</AlertCount>
|
||||
{showBulkActions ? (
|
||||
<>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<StatefulAlertStatusBulkActions
|
||||
data-test-subj="bulk-actions"
|
||||
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,
|
||||
}),
|
||||
[additionalControls, browserFields, columnHeaders, id]
|
||||
[
|
||||
id,
|
||||
alertCountText,
|
||||
totalItems,
|
||||
filterStatus,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
additionalControls,
|
||||
showBulkActions,
|
||||
onAlertStatusActionSuccess,
|
||||
onAlertStatusActionFailure,
|
||||
refetch,
|
||||
]
|
||||
);
|
||||
|
||||
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',
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
DataProvider,
|
||||
RowRenderer,
|
||||
AlertStatus,
|
||||
} from '../../../../common/types/timeline';
|
||||
import {
|
||||
esQuery,
|
||||
|
@ -40,20 +41,15 @@ import { HeaderSection } from '../header_section';
|
|||
import { StatefulBody } from '../body';
|
||||
import { Footer, footerHeight } from '../footer';
|
||||
import { LastUpdatedAt } from '../..';
|
||||
import { AlertCount, SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||
import * as i18n from './translations';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||
import * as i18n from '../translations';
|
||||
import { ExitFullScreen } from '../../exit_full_screen';
|
||||
import { Sort } from '../body/sort';
|
||||
import { InspectButtonContainer } from '../../inspect';
|
||||
|
||||
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
||||
const UTILITY_BAR_HEIGHT = 19; // px
|
||||
const COMPACT_HEADER_HEIGHT = 36; // px
|
||||
|
||||
const UtilityBar = styled.div`
|
||||
height: ${UTILITY_BAR_HEIGHT}px;
|
||||
`;
|
||||
|
||||
const TitleText = styled.span`
|
||||
margin-right: 12px;
|
||||
`;
|
||||
|
@ -114,6 +110,7 @@ export interface TGridIntegratedProps {
|
|||
filters: Filter[];
|
||||
globalFullScreen: boolean;
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
filterStatus?: AlertStatus;
|
||||
height?: number;
|
||||
id: TimelineId;
|
||||
indexNames: string[];
|
||||
|
@ -133,8 +130,8 @@ export interface TGridIntegratedProps {
|
|||
utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode;
|
||||
// If truthy, the graph viewer (Resolver) is showing
|
||||
graphEventId: string | undefined;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
leadingControlColumns?: ControlColumnProps[];
|
||||
trailingControlColumns?: ControlColumnProps[];
|
||||
data?: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
|
@ -148,6 +145,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
filters,
|
||||
globalFullScreen,
|
||||
headerFilterGroup,
|
||||
filterStatus,
|
||||
id,
|
||||
indexNames,
|
||||
indexPattern,
|
||||
|
@ -255,13 +253,6 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
[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)), [
|
||||
deletedEventIds,
|
||||
events,
|
||||
|
@ -301,9 +292,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
>
|
||||
{HeaderSectionContent}
|
||||
</HeaderSection>
|
||||
{utilityBar && !resolverIsShowing(graphEventId) && (
|
||||
<UtilityBar>{utilityBar?.(refetch, totalCountMinusDeleted)}</UtilityBar>
|
||||
)}
|
||||
|
||||
<EventsContainerLoading
|
||||
data-timeline-id={id}
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
|
@ -318,7 +307,6 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
<ScrollableFlexItem grow={1}>
|
||||
<StatefulBody
|
||||
activePage={pageInfo.activePage}
|
||||
additionalControls={additionalControls}
|
||||
browserFields={browserFields}
|
||||
data={nonDeletedEvents}
|
||||
id={id}
|
||||
|
@ -332,8 +320,12 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
itemsCount: totalCountMinusDeleted,
|
||||
itemsPerPage,
|
||||
})}
|
||||
totalItems={totalCountMinusDeleted}
|
||||
unit={unit}
|
||||
filterStatus={filterStatus}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<Footer
|
||||
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,
|
||||
RowRenderer,
|
||||
SortColumnTimeline,
|
||||
BulkActionsProp,
|
||||
AlertStatus,
|
||||
} from '../../../../common/types/timeline';
|
||||
import {
|
||||
esQuery,
|
||||
|
@ -29,7 +31,6 @@ import {
|
|||
DataPublicPluginStart,
|
||||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||
import { Refetch } from '../../../store/t_grid/inputs';
|
||||
import { defaultHeaders } from '../body/column_headers/default_headers';
|
||||
import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers';
|
||||
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
|
||||
|
@ -38,21 +39,16 @@ import { HeaderSection } from '../header_section';
|
|||
import { StatefulBody } from '../body';
|
||||
import { Footer, footerHeight } from '../footer';
|
||||
import { LastUpdatedAt } from '../..';
|
||||
import { AlertCount, SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||
import * as i18n from './translations';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||
import * as i18n from '../translations';
|
||||
import { InspectButtonContainer } from '../../inspect';
|
||||
import { useFetchIndex } from '../../../container/source';
|
||||
|
||||
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
||||
const UTILITY_BAR_HEIGHT = 19; // px
|
||||
const COMPACT_HEADER_HEIGHT = 36; // px
|
||||
const STANDALONE_ID = 'standalone-t-grid';
|
||||
const EMPTY_DATA_PROVIDERS: DataProvider[] = [];
|
||||
|
||||
const UtilityBar = styled.div`
|
||||
height: ${UTILITY_BAR_HEIGHT}px;
|
||||
`;
|
||||
|
||||
const TitleText = styled.span`
|
||||
margin-right: 12px;
|
||||
`;
|
||||
|
@ -108,6 +104,7 @@ export interface TGridStandaloneProps {
|
|||
filters: Filter[];
|
||||
footerText: React.ReactNode;
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
filterStatus: AlertStatus;
|
||||
height?: number;
|
||||
indexNames: string[];
|
||||
itemsPerPage: number;
|
||||
|
@ -119,10 +116,10 @@ export interface TGridStandaloneProps {
|
|||
setRefetch: (ref: () => void) => void;
|
||||
start: string;
|
||||
sort: SortColumnTimeline[];
|
||||
utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode;
|
||||
graphEventId?: string;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
bulkActions?: BulkActionsProp;
|
||||
data?: DataPublicPluginStart;
|
||||
unit: (total: number) => React.ReactNode;
|
||||
}
|
||||
|
@ -136,6 +133,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
filters,
|
||||
footerText,
|
||||
headerFilterGroup,
|
||||
filterStatus,
|
||||
indexNames,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
|
@ -146,7 +144,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
setRefetch,
|
||||
start,
|
||||
sort,
|
||||
utilityBar,
|
||||
graphEventId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
|
@ -166,6 +163,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
queryFields,
|
||||
title,
|
||||
} = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? ''));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(tGridActions.updateIsLoading({ id: STANDALONE_ID, isLoading: isQueryLoading }));
|
||||
}, [dispatch, isQueryLoading]);
|
||||
|
@ -237,13 +235,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
[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)), [
|
||||
deletedEventIds,
|
||||
events,
|
||||
|
@ -310,9 +301,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
>
|
||||
{HeaderSectionContent}
|
||||
</HeaderSection>
|
||||
{utilityBar && !resolverIsShowing(graphEventId) && (
|
||||
<UtilityBar>{utilityBar?.(refetch, totalCountMinusDeleted)}</UtilityBar>
|
||||
)}
|
||||
|
||||
<EventsContainerLoading
|
||||
data-timeline-id={STANDALONE_ID}
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
|
@ -327,7 +316,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
<ScrollableFlexItem grow={1}>
|
||||
<StatefulBody
|
||||
activePage={pageInfo.activePage}
|
||||
additionalControls={additionalControls}
|
||||
browserFields={browserFields}
|
||||
data={nonDeletedEvents}
|
||||
id={STANDALONE_ID}
|
||||
|
@ -341,8 +329,12 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
itemsCount: totalCountMinusDeleted,
|
||||
itemsPerPage: itemsPerPageStore,
|
||||
})}
|
||||
totalItems={totalCountMinusDeleted}
|
||||
unit={unit}
|
||||
filterStatus={filterStatus}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<Footer
|
||||
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`
|
||||
color: ${({ theme }) => theme.eui.euiTextColors.subdued};
|
||||
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}
|
||||
color="text"
|
||||
data-test-subj="show-field-browser"
|
||||
iconType="listAdd"
|
||||
iconType="tableOfContents"
|
||||
onClick={onShow}
|
||||
size="xs"
|
||||
>
|
||||
|
|
|
@ -18,3 +18,99 @@ export const EVENTS_TABLE_ARIA_LABEL = ({
|
|||
values: { activePage, 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;
|
||||
/** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
|
||||
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[]>;
|
||||
savedObjectId: string | null;
|
||||
version: string | null;
|
||||
|
|
|
@ -23897,10 +23897,6 @@
|
|||
"xpack.timelines.draggables.field.typeLabel": "型",
|
||||
"xpack.timelines.draggables.field.viewCategoryTooltip": "カテゴリーを表示します",
|
||||
"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.footer.autoRefreshActiveDescription": "自動更新アクション",
|
||||
"xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。",
|
||||
|
|
|
@ -24449,11 +24449,6 @@
|
|||
"xpack.timelines.draggables.field.typeLabel": "类型",
|
||||
"xpack.timelines.draggables.field.viewCategoryTooltip": "查看类别",
|
||||
"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.footer.autoRefreshActiveDescription": "自动刷新已启用",
|
||||
"xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。",
|
||||
|
|
|
@ -82,6 +82,7 @@ const AppRoot = React.memo(
|
|||
setRefetch,
|
||||
start: '',
|
||||
rowRenderers: [],
|
||||
filterStatus: 'open',
|
||||
unit: (n: number) => `${n}`,
|
||||
})) ??
|
||||
null}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue