[Security Solution][Detections] Moves last updated info inline with status filter (#108096)

This commit is contained in:
Davis Plumlee 2021-08-14 01:24:00 -04:00 committed by GitHub
parent 94d16f8882
commit 5f947c2531
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 20 deletions

View file

@ -22,6 +22,7 @@ import { useUserData } from '../../components/user_info';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { createStore, State } from '../../../common/store';
import { mockHistory, Router } from '../../../common/mock/router';
import { mockTimelines } from '../../../common/mock/mock_timelines_plugin';
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar and QueryBar
@ -56,6 +57,33 @@ jest.mock('../../components/alerts_info', () => ({
useAlertInfo: jest.fn().mockReturnValue([]),
}));
jest.mock('../../../common/lib/kibana', () => {
const original = jest.requireActual('../../../common/lib/kibana');
return {
...original,
useUiSetting$: jest.fn().mockReturnValue([]),
useKibana: () => ({
services: {
application: {
navigateToUrl: jest.fn(),
},
timelines: { ...mockTimelines },
data: {
query: {
filterManager: jest.fn().mockReturnValue({}),
},
},
},
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
};
});
const state: State = {
...mockGlobalState,
};

View file

@ -94,6 +94,9 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
const graphEventId = useShallowEqualSelector(
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId
);
const updatedAt = useShallowEqualSelector(
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).updated
);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
@ -125,7 +128,10 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false);
const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false);
const loading = userInfoLoading || listsConfigLoading;
const { navigateToUrl } = useKibana().services.application;
const {
application: { navigateToUrl },
timelines: timelinesUi,
} = useKibana().services;
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
const updateDateRangeCallback = useCallback<UpdateDateRange>(
@ -289,7 +295,14 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
</LinkAnchor>
</DetectionEngineHeaderPage>
<EuiHorizontalRule margin="m" />
<AlertsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<AlertsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{timelinesUi.getLastUpdated({ updatedAt: updatedAt || 0, showUpdating: loading })}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup wrap>
<EuiFlexItem grow={2}>

View file

@ -23,6 +23,7 @@ import { useUserData } from '../../../../components/user_info';
import { useSourcererScope } from '../../../../../common/containers/sourcerer';
import { useParams } from 'react-router-dom';
import { mockHistory, Router } from '../../../../../common/mock/router';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar and QueryBar
@ -55,6 +56,34 @@ jest.mock('react-router-dom', () => {
};
});
jest.mock('../../../../../common/lib/kibana', () => {
const original = jest.requireActual('../../../../../common/lib/kibana');
return {
...original,
useUiSetting$: jest.fn().mockReturnValue([]),
useKibana: () => ({
services: {
application: {
navigateToUrl: jest.fn(),
capabilities: { actions: jest.fn().mockReturnValue({}) },
},
timelines: { ...mockTimelines },
data: {
query: {
filterManager: jest.fn().mockReturnValue({}),
},
},
},
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
};
});
const state: State = {
...mockGlobalState,
};

View file

@ -59,7 +59,6 @@ import { AlertsHistogramPanel } from '../../../../components/alerts_kpis/alerts_
import { AlertsTable } from '../../../../components/alerts_table';
import { useUserData } from '../../../../components/user_info';
import { OverviewEmpty } from '../../../../../overview/components/overview_empty';
import { useAlertInfo } from '../../../../components/alerts_info';
import { StepDefineRule } from '../../../../components/rules/step_define_rule';
import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule';
import {
@ -178,6 +177,9 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
(state) =>
(getTimeline(state, TimelineId.detectionsRulesDetailsPage) ?? timelineDefaults).graphEventId
);
const updatedAt = useShallowEqualSelector(
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).updated
);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
@ -234,7 +236,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
defineRuleData: null,
scheduleRuleData: null,
};
const [lastAlerts] = useAlertInfo({ ruleId });
const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false);
const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false);
const mlCapabilities = useMlCapabilities();
@ -255,6 +256,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
application: {
capabilities: { actions },
},
timelines: timelinesUi,
},
} = useKibana();
const hasActionsPrivileges = useMemo(() => {
@ -649,16 +651,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
}}
border
subtitle={subTitle}
subtitle2={[
...(lastAlerts != null
? [
<>
{detectionI18n.LAST_ALERT}
{': '}
{lastAlerts}
</>,
]
: []),
subtitle2={
<>
<EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="flexStart">
<EuiFlexItem grow={false}>
@ -667,8 +660,8 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
</EuiFlexItem>
{ruleStatusInfo}
</EuiFlexGroup>
</>,
]}
</>
}
title={title}
badgeOptions={badgeOptions}
>
@ -759,8 +752,18 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
</Display>
{ruleDetailTab === RuleDetailTabs.alerts && (
<>
<AlertsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<AlertsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{timelinesUi.getLastUpdated({
updatedAt: updatedAt || 0,
showUpdating: loading,
})}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<Display show={!globalFullScreen}>
<AlertsHistogramPanel
filters={alertMergedFilters}

View file

@ -180,6 +180,13 @@ export const useTimelineEvents = ({
wrappedLoadPage(0);
}, [wrappedLoadPage]);
const setUpdated = useCallback(
(updatedAt: number) => {
dispatch(timelineActions.setTimelineUpdatedAt({ id, updated: updatedAt }));
},
[dispatch, id]
);
const [timelineResponse, setTimelineResponse] = useState<TimelineArgs>({
id,
inspect: {
@ -230,6 +237,7 @@ export const useTimelineEvents = ({
totalCount: response.totalCount,
updatedAt: Date.now(),
};
setUpdated(newTimelineResponse.updatedAt);
if (id === TimelineId.active) {
activeTimeline.setExpandedDetail({});
activeTimeline.setPageName(pageName);
@ -303,7 +311,17 @@ export const useTimelineEvents = ({
asyncSearch();
refetch.current = asyncSearch;
},
[data.search, id, addWarning, addError, pageName, refetchGrid, skip, wrappedLoadPage]
[
pageName,
skip,
id,
data.search,
setUpdated,
addWarning,
addError,
refetchGrid,
wrappedLoadPage,
]
);
useEffect(() => {

View file

@ -67,6 +67,10 @@ export const createTimeline = actionCreator<TimelinePersistInput>('CREATE_TIMELI
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
export const setTimelineUpdatedAt = actionCreator<{ id: string; updated: number }>(
'SET_TIMELINE_UPDATED_AT'
);
export const removeProvider = actionCreator<{
id: string;
providerId: string;

View file

@ -46,6 +46,7 @@ import {
updateTitleAndDescription,
toggleModalSaveTimeline,
updateEqlOptions,
setTimelineUpdatedAt,
} from './actions';
import {
addNewTimeline,
@ -372,4 +373,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateTimelineShowTimeline({ id, show, timelineById: state.timelineById }),
}))
.case(setTimelineUpdatedAt, (state, { id, updated }) => ({
...state,
timelineById: {
...state.timelineById,
[id]: {
...state.timelineById[id],
updated,
},
},
}))
.build();

View file

@ -159,6 +159,13 @@ export const useTimelineEvents = ({
wrappedLoadPage(0);
}, [wrappedLoadPage]);
const setUpdated = useCallback(
(updatedAt: number) => {
dispatch(tGridActions.setTimelineUpdatedAt({ id, updated: updatedAt }));
},
[dispatch, id]
);
const [timelineResponse, setTimelineResponse] = useState<TimelineArgs>({
id,
inspect: {
@ -212,6 +219,7 @@ export const useTimelineEvents = ({
totalCount: response.totalCount,
updatedAt: Date.now(),
};
setUpdated(newTimelineResponse.updatedAt);
return newTimelineResponse;
});
searchSubscription$.current.unsubscribe();
@ -237,7 +245,7 @@ export const useTimelineEvents = ({
asyncSearch();
refetch.current = asyncSearch;
},
[data, addWarning, addError, skip]
[skip, data, setUpdated, addWarning, addError]
);
useEffect(() => {

View file

@ -102,6 +102,10 @@ export const setTGridSelectAll = actionCreator<{ id: string; selectAll: boolean
'SET_TGRID_SELECT_ALL'
);
export const setTimelineUpdatedAt = actionCreator<{ id: string; updated: number }>(
'SET_TIMELINE_UPDATED_AT'
);
export const addProviderToTimeline = actionCreator<{ id: string; dataProvider: DataProvider }>(
'ADD_PROVIDER_TO_TIMELINE'
);

View file

@ -21,6 +21,7 @@ import {
setOpenAddToExistingCase,
setOpenAddToNewCase,
setSelected,
setTimelineUpdatedAt,
toggleDetailPanel,
updateColumns,
updateIsLoading,
@ -237,4 +238,14 @@ export const tGridReducer = reducerWithInitialState(initialTGridState)
},
},
}))
.case(setTimelineUpdatedAt, (state, { id, updated }) => ({
...state,
timelineById: {
...state.timelineById,
[id]: {
...state.timelineById[id],
updated,
},
},
}))
.build();