[Security Solution][Endpoint] Use super date picker instead of date range picker (#108722)
* Use super date picker instead of date range picker
fixes elastic/security-team/issues/1571
* fix test target
Super date picker's `data-test-subj` prop gets garbled and doesn't show up in rendered DOM. In other words, the component is entirely void of a data-test-subj attribute.
* make auto refresh work!!
fixes https://github.com/elastic/security-team/issues/1571
* set max width as per mock
fixes elastic/security-team/issues/1571
* show a callout to inform users to select different date ranges
fixes elastic/security-team/issues/1571
* persist recently used date ranges on the component only
fixes elastic/security-team/issues/1571
* use commonly used ranges from default common security solution ranges
fixes elastic/security-team/issues/1571
* Better align date picker
* full width panel for date picker so content flows below it
review comments
* mock time picker settings for tests
* use eui token for bg color
review comment
* persist recently used dates
fixes elastic/security-team/issues/1571
* persist date range selection over new endpoint selection
review comments
* remove obsolete local state since update button is not visible.
review comments
* fix bg color for dark mode and relative path
* update relative path
review comments
* cleanup - the action doesn't allow for undefined start and end dates anyway
refs 28a859ab3a
* fix types after sync
* update test title
* add a test for callout when empty data
* fix lint
* show update button when dates are changed
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
df8ed81195
commit
5b4d265571
|
@ -23,8 +23,8 @@ export const EndpointActionLogRequestSchema = {
|
|||
query: schema.object({
|
||||
page: schema.number({ defaultValue: 1, min: 1 }),
|
||||
page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }),
|
||||
start_date: schema.maybe(schema.string()),
|
||||
end_date: schema.maybe(schema.string()),
|
||||
start_date: schema.string(),
|
||||
end_date: schema.string(),
|
||||
}),
|
||||
params: schema.object({
|
||||
agent_id: schema.string(),
|
||||
|
|
|
@ -65,8 +65,8 @@ export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse;
|
|||
export interface ActivityLog {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
data: ActivityLogEntry[];
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,8 @@ export const endpointActivityLogHttpMock = httpHandlerMockFactory<EndpointActivi
|
|||
body: {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
data: [
|
||||
{
|
||||
type: 'response',
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { EuiSuperDatePickerRecentRange } from '@elastic/eui';
|
||||
import {
|
||||
HostResultList,
|
||||
HostInfo,
|
||||
|
@ -161,11 +162,23 @@ export interface EndpointDetailsActivityLogUpdatePaging {
|
|||
disabled?: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserUpdatedActivityLogRefreshOptions {
|
||||
type: 'userUpdatedActivityLogRefreshOptions';
|
||||
payload: {
|
||||
autoRefreshOptions: { enabled: boolean; duration: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserUpdatedActivityLogRecentlyUsedDateRanges {
|
||||
type: 'userUpdatedActivityLogRecentlyUsedDateRanges';
|
||||
payload: EuiSuperDatePickerRecentRange[];
|
||||
}
|
||||
|
||||
export interface EndpointDetailsLoad {
|
||||
type: 'endpointDetailsLoad';
|
||||
payload: {
|
||||
|
@ -194,6 +207,8 @@ export type EndpointAction =
|
|||
| EndpointDetailsActivityLogUpdatePaging
|
||||
| EndpointDetailsActivityLogUpdateIsInvalidDateRange
|
||||
| EndpointDetailsActivityLogChanged
|
||||
| UserUpdatedActivityLogRefreshOptions
|
||||
| UserUpdatedActivityLogRecentlyUsedDateRanges
|
||||
| EndpointDetailsLoad
|
||||
| ServerReturnedEndpointPolicyResponse
|
||||
| ServerFailedToReturnEndpointPolicyResponse
|
||||
|
|
|
@ -24,9 +24,14 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => {
|
|||
disabled: false,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
isInvalidDateRange: false,
|
||||
autoRefreshOptions: {
|
||||
enabled: false,
|
||||
duration: DEFAULT_POLL_INTERVAL,
|
||||
},
|
||||
recentlyUsedDateRanges: [],
|
||||
},
|
||||
logData: createUninitialisedResourceState(),
|
||||
},
|
||||
|
|
|
@ -48,7 +48,14 @@ describe('EndpointList store concerns', () => {
|
|||
disabled: false,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
isInvalidDateRange: false,
|
||||
autoRefreshOptions: {
|
||||
enabled: false,
|
||||
duration: DEFAULT_POLL_INTERVAL,
|
||||
},
|
||||
recentlyUsedDateRanges: [],
|
||||
},
|
||||
logData: { type: 'UninitialisedResourceState' },
|
||||
},
|
||||
|
|
|
@ -267,6 +267,8 @@ describe('endpoint list middleware', () => {
|
|||
payload: {
|
||||
page,
|
||||
pageSize: 50,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -311,6 +313,8 @@ describe('endpoint list middleware', () => {
|
|||
expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledWith({
|
||||
path: expect.any(String),
|
||||
query: {
|
||||
end_date: 'now',
|
||||
start_date: 'now-1d',
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
},
|
||||
|
@ -396,6 +400,8 @@ describe('endpoint list middleware', () => {
|
|||
query: {
|
||||
page: 3,
|
||||
page_size: 50,
|
||||
start_date: 'now-1d',
|
||||
end_date: 'now',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -640,12 +640,12 @@ async function endpointDetailsActivityLogChangedMiddleware({
|
|||
});
|
||||
|
||||
try {
|
||||
const { page, pageSize } = getActivityLogDataPaging(getState());
|
||||
const { page, pageSize, startDate, endDate } = getActivityLogDataPaging(getState());
|
||||
const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, {
|
||||
agent_id: selectedAgent(getState()),
|
||||
});
|
||||
const activityLog = await coreStart.http.get<ActivityLog>(route, {
|
||||
query: { page, page_size: pageSize },
|
||||
query: { page, page_size: pageSize, start_date: startDate, end_date: endDate },
|
||||
});
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogChanged',
|
||||
|
|
|
@ -24,6 +24,7 @@ import { AppAction } from '../../../../common/store/actions';
|
|||
import { ImmutableReducer } from '../../../../common/store';
|
||||
import { Immutable } from '../../../../../common/endpoint/types';
|
||||
import { createUninitialisedResourceState, isUninitialisedResourceState } from '../../../state';
|
||||
import { DEFAULT_POLL_INTERVAL } from '../../../common/constants';
|
||||
|
||||
type StateReducer = ImmutableReducer<EndpointState, AppAction>;
|
||||
type CaseReducer<T extends AppAction> = (
|
||||
|
@ -172,7 +173,11 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
},
|
||||
},
|
||||
};
|
||||
} else if (action.type === 'endpointDetailsActivityLogUpdatePaging') {
|
||||
} else if (
|
||||
action.type === 'endpointDetailsActivityLogUpdatePaging' ||
|
||||
action.type === 'endpointDetailsActivityLogUpdateIsInvalidDateRange' ||
|
||||
action.type === 'userUpdatedActivityLogRefreshOptions'
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
endpointDetails: {
|
||||
|
@ -186,7 +191,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
},
|
||||
},
|
||||
};
|
||||
} else if (action.type === 'endpointDetailsActivityLogUpdateIsInvalidDateRange') {
|
||||
} else if (action.type === 'userUpdatedActivityLogRecentlyUsedDateRanges') {
|
||||
return {
|
||||
...state,
|
||||
endpointDetails: {
|
||||
|
@ -195,7 +200,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
...state.endpointDetails.activityLog,
|
||||
paging: {
|
||||
...state.endpointDetails.activityLog.paging,
|
||||
...action.payload,
|
||||
recentlyUsedDateRanges: action.payload,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -315,9 +320,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
logData: createUninitialisedResourceState(),
|
||||
paging: {
|
||||
disabled: false,
|
||||
isInvalidDateRange: false,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
isInvalidDateRange: false,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
autoRefreshOptions: {
|
||||
enabled: false,
|
||||
duration: DEFAULT_POLL_INTERVAL,
|
||||
},
|
||||
recentlyUsedDateRanges: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -337,7 +349,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
activityLog,
|
||||
activityLog: {
|
||||
...activityLog,
|
||||
paging: {
|
||||
...activityLog.paging,
|
||||
startDate: state.endpointDetails.activityLog.paging.startDate,
|
||||
endDate: state.endpointDetails.activityLog.paging.endDate,
|
||||
recentlyUsedDateRanges:
|
||||
state.endpointDetails.activityLog.paging.recentlyUsedDateRanges,
|
||||
},
|
||||
},
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsError: undefined,
|
||||
|
@ -355,7 +376,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
activityLog,
|
||||
activityLog: {
|
||||
...activityLog,
|
||||
paging: {
|
||||
...activityLog.paging,
|
||||
startDate: state.endpointDetails.activityLog.paging.startDate,
|
||||
endDate: state.endpointDetails.activityLog.paging.endDate,
|
||||
recentlyUsedDateRanges:
|
||||
state.endpointDetails.activityLog.paging.recentlyUsedDateRanges,
|
||||
},
|
||||
},
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsLoading: !isNotLoadingDetails,
|
||||
|
@ -372,7 +402,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
activityLog,
|
||||
activityLog: {
|
||||
...activityLog,
|
||||
paging: {
|
||||
...activityLog.paging,
|
||||
startDate: state.endpointDetails.activityLog.paging.startDate,
|
||||
endDate: state.endpointDetails.activityLog.paging.endDate,
|
||||
recentlyUsedDateRanges:
|
||||
state.endpointDetails.activityLog.paging.recentlyUsedDateRanges,
|
||||
},
|
||||
},
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsLoading: true,
|
||||
|
@ -391,7 +430,15 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
activityLog,
|
||||
activityLog: {
|
||||
...activityLog,
|
||||
paging: {
|
||||
...activityLog.paging,
|
||||
startDate: state.endpointDetails.activityLog.paging.startDate,
|
||||
endDate: state.endpointDetails.activityLog.paging.endDate,
|
||||
recentlyUsedDateRanges: state.endpointDetails.activityLog.paging.recentlyUsedDateRanges,
|
||||
},
|
||||
},
|
||||
hostDetails: {
|
||||
...state.endpointDetails.hostDetails,
|
||||
detailsError: undefined,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSuperDatePickerRecentRange } from '@elastic/eui';
|
||||
import {
|
||||
ActivityLog,
|
||||
HostInfo,
|
||||
|
@ -41,9 +42,14 @@ export interface EndpointState {
|
|||
disabled?: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isInvalidDateRange: boolean;
|
||||
autoRefreshOptions: {
|
||||
enabled: boolean;
|
||||
duration: number;
|
||||
};
|
||||
recentlyUsedDateRanges: EuiSuperDatePickerRecentRange[];
|
||||
};
|
||||
logData: AsyncResourceState<ActivityLog>;
|
||||
};
|
||||
|
|
|
@ -10,12 +10,14 @@ import { getIsInvalidDateRange } from './utils';
|
|||
|
||||
describe('utils', () => {
|
||||
describe('getIsInvalidDateRange', () => {
|
||||
it('should return FALSE when either dates are undefined', () => {
|
||||
expect(getIsInvalidDateRange({})).toBe(false);
|
||||
expect(getIsInvalidDateRange({ startDate: moment().subtract(1, 'd').toISOString() })).toBe(
|
||||
false
|
||||
);
|
||||
expect(getIsInvalidDateRange({ endDate: moment().toISOString() })).toBe(false);
|
||||
it('should return FALSE when startDate is before endDate', () => {
|
||||
expect(getIsInvalidDateRange({ startDate: 'now-1d', endDate: 'now' })).toBe(false);
|
||||
expect(
|
||||
getIsInvalidDateRange({
|
||||
startDate: moment().subtract(1, 'd').toISOString(),
|
||||
endDate: moment().toISOString(),
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return TRUE when startDate is after endDate', () => {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import moment from 'moment';
|
||||
import { HostInfo, HostMetadata } from '../../../../common/endpoint/types';
|
||||
|
||||
|
@ -29,12 +30,12 @@ export const getIsInvalidDateRange = ({
|
|||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}) => {
|
||||
if (startDate && endDate) {
|
||||
const start = moment(startDate);
|
||||
const end = moment(endDate);
|
||||
const start = moment(dateMath.parse(startDate));
|
||||
const end = moment(dateMath.parse(endDate));
|
||||
if (start.isValid() && end.isValid()) {
|
||||
return start.isAfter(end);
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -8,95 +8,140 @@
|
|||
import { useDispatch } from 'react-redux';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import moment from 'moment';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSuperDatePicker,
|
||||
EuiSuperDatePickerRecentRange,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import * as i18 from '../../../translations';
|
||||
import { useEndpointSelector } from '../../../hooks';
|
||||
import { getActivityLogDataPaging } from '../../../../store/selectors';
|
||||
import {
|
||||
getActivityLogDataPaging,
|
||||
getActivityLogRequestLoading,
|
||||
} from '../../../../store/selectors';
|
||||
import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../../../common/constants';
|
||||
import { useUiSetting$ } from '../../../../../../../common/lib/kibana';
|
||||
|
||||
interface Range {
|
||||
from: string;
|
||||
to: string;
|
||||
display: string;
|
||||
}
|
||||
|
||||
const DatePickerWrapper = styled.div`
|
||||
width: ${(props) => props.theme.eui.fractions.single.percentage};
|
||||
background: white;
|
||||
max-width: 350px;
|
||||
`;
|
||||
const StickyFlexItem = styled(EuiFlexItem)`
|
||||
max-width: 350px;
|
||||
background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`};
|
||||
position: sticky;
|
||||
top: ${(props) => props.theme.eui.euiSizeM};
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding: ${(props) => `0 ${props.theme.eui.paddingSizes.m}`};
|
||||
padding: ${(props) => `${props.theme.eui.paddingSizes.m}`};
|
||||
`;
|
||||
|
||||
export const DateRangePicker = memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
const { page, pageSize, startDate, endDate, isInvalidDateRange } = useEndpointSelector(
|
||||
getActivityLogDataPaging
|
||||
);
|
||||
const {
|
||||
page,
|
||||
pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
autoRefreshOptions,
|
||||
recentlyUsedDateRanges,
|
||||
} = useEndpointSelector(getActivityLogDataPaging);
|
||||
|
||||
const onChangeStartDate = useCallback(
|
||||
(date) => {
|
||||
const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading);
|
||||
|
||||
const dispatchActionUpdateActivityLogPaging = useCallback(
|
||||
async ({ start, end }) => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: false,
|
||||
page,
|
||||
pageSize,
|
||||
startDate: date ? date?.toISOString() : undefined,
|
||||
endDate: endDate ? endDate : undefined,
|
||||
startDate: dateMath.parse(start)?.toISOString(),
|
||||
endDate: dateMath.parse(end)?.toISOString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, endDate, page, pageSize]
|
||||
[dispatch, page, pageSize]
|
||||
);
|
||||
|
||||
const onChangeEndDate = useCallback(
|
||||
(date) => {
|
||||
const onRefreshChange = useCallback(
|
||||
(evt) => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
type: 'userUpdatedActivityLogRefreshOptions',
|
||||
payload: {
|
||||
disabled: false,
|
||||
page,
|
||||
pageSize,
|
||||
startDate: startDate ? startDate : undefined,
|
||||
endDate: date ? date.toISOString() : undefined,
|
||||
autoRefreshOptions: { enabled: !evt.isPaused, duration: evt.refreshInterval },
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, startDate, page, pageSize]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'endpointDetailsActivityLogUpdatePaging',
|
||||
payload: {
|
||||
disabled: false,
|
||||
page,
|
||||
pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
});
|
||||
}, [dispatch, page, pageSize, startDate, endDate]);
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
({ start: newStart, end: newEnd }) => {
|
||||
const newRecentlyUsedDateRanges = [
|
||||
{ start: newStart, end: newEnd },
|
||||
...recentlyUsedDateRanges
|
||||
.filter(
|
||||
(recentlyUsedRange) =>
|
||||
!(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd)
|
||||
)
|
||||
.slice(0, 9),
|
||||
];
|
||||
dispatch({
|
||||
type: 'userUpdatedActivityLogRecentlyUsedDateRanges',
|
||||
payload: newRecentlyUsedDateRanges,
|
||||
});
|
||||
|
||||
dispatchActionUpdateActivityLogPaging({ start: newStart, end: newEnd });
|
||||
},
|
||||
[dispatch, recentlyUsedDateRanges, dispatchActionUpdateActivityLogPaging]
|
||||
);
|
||||
|
||||
const [quickRanges] = useUiSetting$<Range[]>(DEFAULT_TIMEPICKER_QUICK_RANGES);
|
||||
const commonlyUsedRanges = !quickRanges.length
|
||||
? []
|
||||
: quickRanges.map(({ from, to, display }) => ({
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
}));
|
||||
|
||||
return (
|
||||
<StickyFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd" responsive>
|
||||
<DatePickerWrapper>
|
||||
<EuiFlexGroup justifyContent="flexStart" responsive>
|
||||
<DatePickerWrapper data-test-subj="activityLogSuperDatePicker">
|
||||
<EuiFlexItem>
|
||||
<EuiDatePickerRange
|
||||
fullWidth={true}
|
||||
data-test-subj="activityLogDateRangePicker"
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
aria-label="Start date"
|
||||
endDate={endDate ? moment(endDate) : undefined}
|
||||
isInvalid={isInvalidDateRange}
|
||||
onChange={onChangeStartDate}
|
||||
placeholderText={i18.ACTIVITY_LOG.datePicker.startDate}
|
||||
selected={startDate ? moment(startDate) : undefined}
|
||||
showTimeSelect
|
||||
startDate={startDate ? moment(startDate) : undefined}
|
||||
/>
|
||||
}
|
||||
endDateControl={
|
||||
<EuiDatePicker
|
||||
aria-label="End date"
|
||||
endDate={endDate ? moment(endDate) : undefined}
|
||||
isInvalid={isInvalidDateRange}
|
||||
onChange={onChangeEndDate}
|
||||
placeholderText={i18.ACTIVITY_LOG.datePicker.endDate}
|
||||
selected={endDate ? moment(endDate) : undefined}
|
||||
showTimeSelect
|
||||
startDate={startDate ? moment(startDate) : undefined}
|
||||
/>
|
||||
}
|
||||
<EuiSuperDatePicker
|
||||
isLoading={activityLogLoading}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
end={dateMath.parse(endDate)?.toISOString()}
|
||||
isPaused={!autoRefreshOptions.enabled}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefreshChange={onRefreshChange}
|
||||
refreshInterval={autoRefreshOptions.duration}
|
||||
onRefresh={onRefresh}
|
||||
recentlyUsedRanges={recentlyUsedDateRanges as EuiSuperDatePickerRecentRange[]}
|
||||
start={dateMath.parse(startDate)?.toISOString()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</DatePickerWrapper>
|
||||
|
|
|
@ -9,11 +9,13 @@ import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingContent,
|
||||
EuiEmptyPrompt,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { LogEntry } from './components/log_entry';
|
||||
|
@ -114,6 +116,17 @@ export const EndpointActivityLog = memo(
|
|||
<>
|
||||
<DateRangePicker />
|
||||
<EuiFlexItem grow={true}>
|
||||
{!isPagingDisabled && activityLogLoaded && !activityLogData.length && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
data-test-subj="activityLogNoDataCallout"
|
||||
size="s"
|
||||
title={i18.ACTIVITY_LOG.LogEntry.dateRangeMessage}
|
||||
iconType="alert"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{activityLogLoaded &&
|
||||
activityLogData.map((logEntry) => (
|
||||
<LogEntry key={`${logEntry.item.id}`} logEntry={logEntry} />
|
||||
|
|
|
@ -22,6 +22,8 @@ export const dummyEndpointActivityLog = (
|
|||
data: {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: moment().subtract(5, 'day').fromNow().toString(),
|
||||
endDate: moment().toString(),
|
||||
data: [
|
||||
{
|
||||
type: 'action',
|
||||
|
|
|
@ -10,6 +10,8 @@ import * as reactTestingLibrary from '@testing-library/react';
|
|||
import { EndpointList } from './index';
|
||||
import '../../../../common/mock/match_media';
|
||||
|
||||
import { createUseUiSetting$Mock } from '../../../../../public/common/lib/kibana/kibana_react.mock';
|
||||
|
||||
import {
|
||||
mockEndpointDetailsApiResult,
|
||||
mockEndpointResultList,
|
||||
|
@ -28,7 +30,7 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_da
|
|||
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
|
||||
import { mockPolicyResultList } from '../../policy/store/test_mock_utils';
|
||||
import { getEndpointDetailsPath } from '../../../common/routing';
|
||||
import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kibana';
|
||||
import { KibanaServices, useKibana, useToasts, useUiSetting$ } from '../../../../common/lib/kibana';
|
||||
import { hostIsolationHttpMocks } from '../../../../common/lib/endpoint_isolation/mocks';
|
||||
import {
|
||||
createFailedResourceState,
|
||||
|
@ -40,7 +42,11 @@ import {
|
|||
import { getCurrentIsolationRequestState } from '../store/selectors';
|
||||
import { licenseService } from '../../../../common/hooks/use_license';
|
||||
import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator';
|
||||
import { APP_PATH, MANAGEMENT_PATH } from '../../../../../common/constants';
|
||||
import {
|
||||
APP_PATH,
|
||||
MANAGEMENT_PATH,
|
||||
DEFAULT_TIMEPICKER_QUICK_RANGES,
|
||||
} from '../../../../../common/constants';
|
||||
import { TransformStats, TRANSFORM_STATE } from '../types';
|
||||
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
|
||||
|
||||
|
@ -63,6 +69,59 @@ jest.mock('../../policy/store/services/ingest', () => {
|
|||
sendGetEndpointSecurityPackage: () => Promise.resolve({}),
|
||||
};
|
||||
});
|
||||
const mockUseUiSetting$ = useUiSetting$ as jest.Mock;
|
||||
const timepickerRanges = [
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
display: 'Today',
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now/w',
|
||||
display: 'This week',
|
||||
},
|
||||
{
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
display: 'Last 15 minutes',
|
||||
},
|
||||
{
|
||||
from: 'now-30m',
|
||||
to: 'now',
|
||||
display: 'Last 30 minutes',
|
||||
},
|
||||
{
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
display: 'Last 1 hour',
|
||||
},
|
||||
{
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
display: 'Last 24 hours',
|
||||
},
|
||||
{
|
||||
from: 'now-7d',
|
||||
to: 'now',
|
||||
display: 'Last 7 days',
|
||||
},
|
||||
{
|
||||
from: 'now-30d',
|
||||
to: 'now',
|
||||
display: 'Last 30 days',
|
||||
},
|
||||
{
|
||||
from: 'now-90d',
|
||||
to: 'now',
|
||||
display: 'Last 90 days',
|
||||
},
|
||||
{
|
||||
from: 'now-1y',
|
||||
to: 'now',
|
||||
display: 'Last 1 year',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/hooks/use_license');
|
||||
|
@ -759,6 +818,14 @@ describe('when on the endpoint list page', () => {
|
|||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
mockUseUiSetting$.mockImplementation((key, defaultValue) => {
|
||||
const useUiSetting$Mock = createUseUiSetting$Mock();
|
||||
|
||||
return key === DEFAULT_TIMEPICKER_QUICK_RANGES
|
||||
? [timepickerRanges, jest.fn()]
|
||||
: useUiSetting$Mock(key, defaultValue);
|
||||
});
|
||||
|
||||
const fleetActionGenerator = new FleetActionGenerator('seed');
|
||||
const responseData = fleetActionGenerator.generateResponse({
|
||||
agent_id: agentId,
|
||||
|
@ -766,9 +833,12 @@ describe('when on the endpoint list page', () => {
|
|||
const actionData = fleetActionGenerator.generate({
|
||||
agents: [agentId],
|
||||
});
|
||||
|
||||
getMockData = () => ({
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
data: [
|
||||
{
|
||||
type: 'response',
|
||||
|
@ -838,7 +908,7 @@ describe('when on the endpoint list page', () => {
|
|||
expect(emptyState).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should display empty state when no log data', async () => {
|
||||
it('should not display empty state when no log data', async () => {
|
||||
const activityLogTab = await renderResult.findByTestId('activity_log');
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(activityLogTab);
|
||||
|
@ -848,33 +918,17 @@ describe('when on the endpoint list page', () => {
|
|||
dispatchEndpointDetailsActivityLogChanged('success', {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
const emptyState = await renderResult.queryByTestId('activityLogEmpty');
|
||||
expect(emptyState).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should not display empty state with no log data while date range filter is active', async () => {
|
||||
const activityLogTab = await renderResult.findByTestId('activity_log');
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(activityLogTab);
|
||||
});
|
||||
await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
|
||||
reactTestingLibrary.act(() => {
|
||||
dispatchEndpointDetailsActivityLogChanged('success', {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: new Date().toISOString(),
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
const emptyState = await renderResult.queryByTestId('activityLogEmpty');
|
||||
const dateRangePicker = await renderResult.queryByTestId('activityLogDateRangePicker');
|
||||
expect(emptyState).toBe(null);
|
||||
expect(dateRangePicker).not.toBe(null);
|
||||
|
||||
const superDatePicker = await renderResult.queryByTestId('activityLogSuperDatePicker');
|
||||
expect(superDatePicker).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should display activity log when tab is loaded using the URL', async () => {
|
||||
|
@ -895,6 +949,32 @@ describe('when on the endpoint list page', () => {
|
|||
const logEntries = await renderResult.queryAllByTestId('timelineEntry');
|
||||
expect(logEntries.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should display a callout message if no log data', async () => {
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push(
|
||||
`${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=activity_log`
|
||||
);
|
||||
});
|
||||
const changedUrlAction = await userChangedUrlChecker;
|
||||
expect(changedUrlAction.payload.search).toEqual(
|
||||
'?page_index=0&page_size=10&selected_endpoint=1&show=activity_log'
|
||||
);
|
||||
await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged');
|
||||
reactTestingLibrary.act(() => {
|
||||
dispatchEndpointDetailsActivityLogChanged('success', {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
startDate: 'now-1d',
|
||||
endDate: 'now',
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
const activityLogCallout = await renderResult.findByTestId('activityLogNoDataCallout');
|
||||
expect(activityLogCallout).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when showing host Policy Response panel', () => {
|
||||
|
|
|
@ -15,20 +15,6 @@ export const ACTIVITY_LOG = {
|
|||
tabTitle: i18n.translate('xpack.securitySolution.endpointDetails.activityLog', {
|
||||
defaultMessage: 'Activity Log',
|
||||
}),
|
||||
datePicker: {
|
||||
startDate: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.datePicker.startDate',
|
||||
{
|
||||
defaultMessage: 'Pick a start date',
|
||||
}
|
||||
),
|
||||
endDate: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.datePicker.endDate',
|
||||
{
|
||||
defaultMessage: 'Pick an end date',
|
||||
}
|
||||
),
|
||||
},
|
||||
LogEntry: {
|
||||
endOfLog: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog',
|
||||
|
@ -36,6 +22,13 @@ export const ACTIVITY_LOG = {
|
|||
defaultMessage: 'Nothing more to show',
|
||||
}
|
||||
),
|
||||
dateRangeMessage: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.logEntry.dateRangeMessage.title',
|
||||
{
|
||||
defaultMessage:
|
||||
'Nothing to show for selected date range, please select another and try again.',
|
||||
}
|
||||
),
|
||||
emptyState: {
|
||||
title: i18n.translate(
|
||||
'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.title',
|
||||
|
|
|
@ -48,19 +48,13 @@ describe('Action Log API', () => {
|
|||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work without query params', () => {
|
||||
it('should not work when no params while requesting with query params', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({});
|
||||
}).not.toThrow();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should work with query params', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({ page: 10, page_size: 100 });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with all query params', () => {
|
||||
it('should work with all required query params', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({
|
||||
page: 10,
|
||||
|
@ -71,24 +65,24 @@ describe('Action Log API', () => {
|
|||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with just startDate', () => {
|
||||
it('should not work without endDate', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday
|
||||
});
|
||||
}).not.toThrow();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should work with just endDate', () => {
|
||||
it('should not work without startDate', () => {
|
||||
expect(() => {
|
||||
EndpointActionLogRequestSchema.query.validate({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
end_date: new Date().toISOString(), // today
|
||||
});
|
||||
}).not.toThrow();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should not work without allowed page and page_size params', () => {
|
||||
|
|
|
@ -31,8 +31,8 @@ export const getAuditLogResponse = async ({
|
|||
elasticAgentId: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
context: SecuritySolutionRequestHandlerContext;
|
||||
logger: Logger;
|
||||
}): Promise<ActivityLog> => {
|
||||
|
@ -71,8 +71,8 @@ const getActivityLog = async ({
|
|||
elasticAgentId: string;
|
||||
size: number;
|
||||
from: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
logger: Logger;
|
||||
}) => {
|
||||
const options = {
|
||||
|
@ -84,13 +84,10 @@ const getActivityLog = async ({
|
|||
|
||||
let actionsResult;
|
||||
let responsesResult;
|
||||
const dateFilters = [];
|
||||
if (startDate) {
|
||||
dateFilters.push({ range: { '@timestamp': { gte: startDate } } });
|
||||
}
|
||||
if (endDate) {
|
||||
dateFilters.push({ range: { '@timestamp': { lte: endDate } } });
|
||||
}
|
||||
const dateFilters = [
|
||||
{ range: { '@timestamp': { gte: startDate } } },
|
||||
{ range: { '@timestamp': { lte: endDate } } },
|
||||
];
|
||||
|
||||
try {
|
||||
// fetch actions with matching agent_id
|
||||
|
|
Loading…
Reference in a new issue