[SecuritySolution][Timeline] Remove timeline.isLoading (#198616)

## Summary

Trying to answer the question of: "Do we still need
`timeline.isLoading`?"

`timeline.isSaving` should be the only indicator for "loading" states of
timeline itself. All other pieces of state that are associated with
timeline that could have a loading state, have their own loading
indicators (e.g. data providers, alert list etc).

Therefore, this PR removes all references to `timeline.isLoading` and
parts of the UI that depended on it.

Places that `timeline.isLoading` was used
([context](https://github.com/elastic/kibana/pull/38185)):
- Blocking drag/drop on data providers
- This is not necessary anymore. Drag/drop works while the underlying
query is being executed.
- Showing a loading state for the alerts table & data provider changes
- Both components have their own loading state, so no extra loading
state is necessary

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jan Monschke 2024-11-18 13:17:57 +01:00 committed by GitHub
parent c14c1205ed
commit 47100291a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 15 additions and 480 deletions

View file

@ -15,7 +15,6 @@ interface Props {
children?: React.ReactNode;
droppableId: string;
height?: string;
isDropDisabled?: boolean;
type?: string;
render?: ({ isDraggingOver }: { isDraggingOver: boolean }) => React.ReactNode;
renderClone?: DraggableChildrenFn;
@ -90,15 +89,7 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string
ReactDndDropTarget.displayName = 'ReactDndDropTarget';
export const DroppableWrapper = React.memo<Props>(
({
children = null,
droppableId,
height = '100%',
isDropDisabled = false,
type,
render = null,
renderClone,
}) => {
({ children = null, droppableId, height = '100%', type, render = null, renderClone }) => {
const DroppableContent = useCallback<DroppableProps['children']>(
(provided, snapshot) => (
<ReactDndDropTarget
@ -116,7 +107,6 @@ export const DroppableWrapper = React.memo<Props>(
return (
<Droppable
isDropDisabled={isDropDisabled}
droppableId={droppableId}
direction={'horizontal'}
type={type}

View file

@ -358,7 +358,6 @@ export const mockGlobalState: State = {
historyIds: [],
isFavorite: false,
isLive: false,
isLoading: false,
kqlMode: 'filter',
kqlQuery: { filterQuery: null },
loadingEventIds: [],

View file

@ -1899,7 +1899,6 @@ export const mockTimelineModel: TimelineModel = {
indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
isSaving: false,
isSelectAllChecked: false,
kqlMode: 'filter',
@ -2084,7 +2083,6 @@ export const defaultTimelineProps: CreateTimelineProps = {
indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
isSaving: false,
isSelectAllChecked: false,
itemsPerPage: 25,

View file

@ -30,7 +30,7 @@ import {
mockGetOneTimelineResult,
mockTimelineData,
} from '../../../common/mock';
import type { CreateTimeline, UpdateTimelineLoading } from './types';
import type { CreateTimeline } from './types';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import type { DataProvider } from '../../../../common/types/timeline';
import { TimelineTypeEnum, TimelineStatusEnum } from '../../../../common/api/timeline';
@ -127,7 +127,6 @@ describe('alert actions', () => {
const anchor = '2020-03-01T17:59:46.349Z';
const unix = moment(anchor).valueOf();
let createTimeline: CreateTimeline;
let updateTimelineIsLoading: UpdateTimelineLoading;
let searchStrategyClient: jest.Mocked<ISearchStart>;
let clock: sinon.SinonFakeTimers;
let mockKibanaServices: jest.Mock;
@ -270,7 +269,6 @@ describe('alert actions', () => {
mockGetExceptionFilter = jest.fn().mockResolvedValue(undefined);
createTimeline = jest.fn() as jest.Mocked<CreateTimeline>;
updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>;
mockKibanaServices = KibanaServices.get as jest.Mock;
fetchMock = jest.fn();
@ -296,28 +294,10 @@ describe('alert actions', () => {
describe('sendAlertToTimelineAction', () => {
describe('timeline id is NOT empty string and apollo client exists', () => {
test('it invokes updateTimelineIsLoading to set to true', async () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: mockEcsDataWithAlert,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
expect(mockGetExceptionFilter).not.toHaveBeenCalled();
expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1);
expect(updateTimelineIsLoading).toHaveBeenCalledWith({
id: TimelineId.active,
isLoading: true,
});
});
test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: mockEcsDataWithAlert,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -407,7 +387,6 @@ describe('alert actions', () => {
indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
isSaving: false,
isSelectAllChecked: false,
itemsPerPage: 25,
@ -477,7 +456,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: mockEcsDataWithAlert,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -496,7 +474,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: mockEcsDataWithAlert,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -505,14 +482,6 @@ describe('alert actions', () => {
delete defaultTimelinePropsWithoutNote.ruleNote;
delete defaultTimelinePropsWithoutNote.ruleAuthor;
expect(updateTimelineIsLoading).toHaveBeenCalledWith({
id: TimelineId.active,
isLoading: true,
});
expect(updateTimelineIsLoading).toHaveBeenCalledWith({
id: TimelineId.active,
isLoading: false,
});
expect(mockGetExceptionFilter).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith({
@ -544,7 +513,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMock,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -552,7 +520,6 @@ describe('alert actions', () => {
const expectedTimelineProps = structuredClone(defaultTimelineProps);
expectedTimelineProps.timeline.excludedRowRendererIds = [];
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
expect(mockGetExceptionFilter).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith(expectedTimelineProps);
@ -574,7 +541,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMock,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -582,7 +548,6 @@ describe('alert actions', () => {
const expectedTimelineProps = structuredClone(defaultTimelineProps);
expectedTimelineProps.timeline.excludedRowRendererIds = [];
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
expect(mockGetExceptionFilter).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith(expectedTimelineProps);
@ -608,12 +573,10 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMock,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
expect(mockGetExceptionFilter).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith({
@ -655,12 +618,10 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMock,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
expect(mockGetExceptionFilter).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith(expectedTimelineProps);
@ -732,7 +693,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMockWithNoTemplateTimeline,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -740,7 +700,6 @@ describe('alert actions', () => {
const expectedFrom = '2021-01-10T21:11:45.839Z';
const expectedTo = '2021-01-10T21:12:45.839Z';
expect(updateTimelineIsLoading).not.toHaveBeenCalled();
expect(mockGetExceptionFilter).toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith({
@ -861,7 +820,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMockWithNoTemplateTimelineAndNoFilters,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -886,7 +844,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMockWithTemplateTimeline,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -894,7 +851,6 @@ describe('alert actions', () => {
const expectedFrom = '2021-01-10T21:11:45.839Z';
const expectedTo = '2021-01-10T21:12:45.839Z';
expect(updateTimelineIsLoading).toHaveBeenCalled();
expect(mockGetExceptionFilter).toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimeline).toHaveBeenCalledWith({
@ -1046,7 +1002,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMockWithNoTemplateTimeline,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});
@ -1141,7 +1096,6 @@ describe('alert actions', () => {
await sendAlertToTimelineAction({
createTimeline,
ecsData: ecsDataMockWithNoTemplateTimeline,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter: mockGetExceptionFilter,
});

View file

@ -936,7 +936,6 @@ export const sendBulkEventsToTimelineAction = async (
export const sendAlertToTimelineAction = async ({
createTimeline,
ecsData: ecs,
updateTimelineIsLoading,
searchStrategyClient,
getExceptionFilter,
}: SendAlertToTimelineActionProps) => {
@ -962,7 +961,6 @@ export const sendAlertToTimelineAction = async ({
// For now we do not want to populate the template timeline if we have alertIds
if (!isEmpty(timelineId)) {
try {
updateTimelineIsLoading({ id: TimelineId.active, isLoading: true });
const [responseTimeline, eventDataResp] = await Promise.all([
getTimelineTemplate(timelineId),
lastValueFrom(
@ -1092,7 +1090,6 @@ export const sendAlertToTimelineAction = async ({
} catch (error) {
/* eslint-disable-next-line no-console */
console.error(error);
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
return createTimeline({
from,
notes: null,

View file

@ -23,7 +23,6 @@ import { useTimelineEventsHandler } from '../../../../timelines/containers';
import { eventsViewerSelector } from '../../../../common/components/events_viewer/selectors';
import type { State } from '../../../../common/store/types';
import { useUpdateTimeline } from '../../../../timelines/components/open_timeline/use_update_timeline';
import { timelineActions } from '../../../../timelines/store';
import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline';
import { INVESTIGATE_BULK_IN_TIMELINE } from '../translations';
import { TimelineId } from '../../../../../common/types/timeline';
@ -141,18 +140,11 @@ export const useAddBulkToTimelineAction = ({
timelineType: TimelineTypeEnum.default,
});
const updateTimelineIsLoading = useCallback(
(payload: Parameters<typeof timelineActions.updateIsLoading>[0]) =>
dispatch(timelineActions.updateIsLoading(payload)),
[dispatch]
);
const updateTimeline = useUpdateTimeline();
const createTimeline = useCallback(
async ({ timeline, ruleNote, timeline: { filters: eventIdFilters } }: CreateTimelineProps) => {
await clearActiveTimeline();
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
updateTimeline({
duplicate: true,
from,
@ -168,7 +160,7 @@ export const useAddBulkToTimelineAction = ({
ruleNote,
});
},
[updateTimeline, updateTimelineIsLoading, clearActiveTimeline, from, to]
[updateTimeline, clearActiveTimeline, from, to]
);
const sendBulkEventsToTimelineHandler = useCallback(

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { ALERT_RULE_EXCEPTIONS_LIST, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
@ -23,7 +22,6 @@ import { createHistoryEntry } from '../../../../common/utils/global_query_string
import { useKibana } from '../../../../common/lib/kibana';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineTypeEnum } from '../../../../../common/api/timeline';
import { timelineActions } from '../../../../timelines/store';
import { sendAlertToTimelineAction } from '../actions';
import { useUpdateTimeline } from '../../../../timelines/components/open_timeline/use_update_timeline';
import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline';
@ -98,7 +96,6 @@ export const useInvestigateInTimeline = ({
const {
data: { search: searchStrategyClient },
} = useKibana().services;
const dispatch = useDispatch();
const { startTransaction } = useStartTransaction();
const { services } = useKibana();
@ -133,12 +130,6 @@ export const useInvestigateInTimeline = ({
[addError, getExceptionFilterFromIds]
);
const updateTimelineIsLoading = useCallback(
(payload: Parameters<typeof timelineActions.updateIsLoading>[0]) =>
dispatch(timelineActions.updateIsLoading(payload)),
[dispatch]
);
const clearActiveTimeline = useCreateTimeline({
timelineId: TimelineId.active,
timelineType: TimelineTypeEnum.default,
@ -153,7 +144,6 @@ export const useInvestigateInTimeline = ({
!newColumns || isEmpty(newColumns) ? defaultUdtHeaders : newColumns;
await clearActiveTimeline();
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
updateTimeline({
duplicate: true,
from: fromTimeline,
@ -173,12 +163,11 @@ export const useInvestigateInTimeline = ({
ruleNote,
});
},
[updateTimeline, updateTimelineIsLoading, clearActiveTimeline]
[updateTimeline, clearActiveTimeline]
);
const investigateInTimelineAlertClick = useCallback(async () => {
createHistoryEntry();
startTransaction({ name: ALERTS_ACTIONS.INVESTIGATE_IN_TIMELINE });
if (onInvestigateInTimelineAlertClick) {
onInvestigateInTimelineAlertClick();
@ -188,7 +177,6 @@ export const useInvestigateInTimeline = ({
createTimeline,
ecsData: ecsRowData,
searchStrategyClient,
updateTimelineIsLoading,
getExceptionFilter,
});
}
@ -198,7 +186,6 @@ export const useInvestigateInTimeline = ({
ecsRowData,
onInvestigateInTimelineAlertClick,
searchStrategyClient,
updateTimelineIsLoading,
getExceptionFilter,
]);

View file

@ -55,13 +55,10 @@ export interface UpdateAlertStatusActionProps {
export interface SendAlertToTimelineActionProps {
createTimeline: CreateTimeline;
ecsData: Ecs | Ecs[];
updateTimelineIsLoading: UpdateTimelineLoading;
searchStrategyClient: ISearchStart;
getExceptionFilter: GetExceptionFilter;
}
export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void;
export interface CreateTimelineProps {
from: string;
timeline: TimelineModel;

View file

@ -121,7 +121,6 @@ describe('useRuleFromTimeline', () => {
expect(result.current.loading).toEqual(true);
await waitForNextUpdate();
expect(setRuleQuery).toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledTimes(2);
});
});
@ -153,16 +152,8 @@ describe('useRuleFromTimeline', () => {
await waitForNextUpdate();
expect(setRuleQuery).toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledTimes(4);
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenNthCalledWith(1, {
type: 'x-pack/security_solution/local/timeline/UPDATE_LOADING',
payload: {
id: 'timeline-1',
isLoading: true,
},
});
expect(mockDispatch).toHaveBeenNthCalledWith(2, {
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW',
payload: {
id: 'timeline',
@ -170,13 +161,6 @@ describe('useRuleFromTimeline', () => {
selectedPatterns: selectedTimeline.data.timeline.indexNames,
},
});
expect(mockDispatch).toHaveBeenNthCalledWith(3, {
type: 'x-pack/security_solution/local/timeline/UPDATE_LOADING',
payload: {
id: 'timeline-1',
isLoading: false,
},
});
});
it('when from timeline data view id === selected data view id and browser fields is not empty, set rule data to match from timeline query', async () => {
@ -347,7 +331,7 @@ describe('useRuleFromTimeline', () => {
const { waitForNextUpdate } = renderHook(() => useRuleFromTimeline(setRuleQuery));
await waitForNextUpdate();
expect(setRuleQuery).toHaveBeenCalled();
expect(mockDispatch).toHaveBeenNthCalledWith(4, {
expect(mockDispatch).toHaveBeenNthCalledWith(2, {
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW',
payload: {
id: 'timeline',

View file

@ -6,14 +6,12 @@
*/
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { timelineDefaults } from '../timelines/store/defaults';
import { APP_UI_ID } from '../../common/constants';
import type { DataProvider } from '../../common/types';
import { TimelineId } from '../../common/types/timeline';
import { TimelineTypeEnum } from '../../common/api/timeline';
import { useStartTransaction } from '../common/lib/apm/use_start_transaction';
import { timelineActions } from '../timelines/store';
import { useCreateTimeline } from '../timelines/hooks/use_create_timeline';
import type { CreateTimelineProps } from '../detections/components/alerts_table/types';
import { useUpdateTimeline } from '../timelines/components/open_timeline/use_update_timeline';
@ -46,15 +44,8 @@ export const useInvestigateInTimeline = ({
from,
to,
}: UseInvestigateInTimelineActionProps) => {
const dispatch = useDispatch();
const { startTransaction } = useStartTransaction();
const updateTimelineIsLoading = useCallback(
(payload: Parameters<typeof timelineActions.updateIsLoading>[0]) =>
dispatch(timelineActions.updateIsLoading(payload)),
[dispatch]
);
const clearActiveTimeline = useCreateTimeline({
timelineId: TimelineId.active,
timelineType: TimelineTypeEnum.default,
@ -65,7 +56,6 @@ export const useInvestigateInTimeline = ({
const createTimeline = useCallback(
async ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => {
await clearActiveTimeline();
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
updateTimeline({
duplicate: true,
from: fromTimeline,
@ -80,7 +70,7 @@ export const useInvestigateInTimeline = ({
ruleNote,
});
},
[updateTimeline, updateTimelineIsLoading, clearActiveTimeline]
[updateTimeline, clearActiveTimeline]
);
const investigateInTimelineClick = useCallback(async () => {

View file

@ -11,7 +11,6 @@ import { waitFor } from '@testing-library/react';
import { mockTimelineResults, mockGetOneTimelineResult } from '../../../common/mock';
import { timelineDefaults } from '../../store/defaults';
import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/actions';
import type { QueryTimelineById } from './helpers';
import {
defaultTimelineToTimelineModel,
@ -646,13 +645,6 @@ describe('helpers', () => {
jest.clearAllMocks();
});
test('dispatch updateIsLoading to true', () => {
expect(dispatchUpdateIsLoading).toBeCalledWith({
id: TimelineId.active,
isLoading: true,
});
});
test('get timeline by Id', () => {
expect(resolveTimeline).toHaveBeenCalled();
});
@ -671,13 +663,6 @@ describe('helpers', () => {
...timeline,
});
});
test('dispatch updateIsLoading to false', () => {
expect(dispatchUpdateIsLoading).toBeCalledWith({
id: TimelineId.active,
isLoading: false,
});
});
});
describe('update a timeline', () => {
@ -706,11 +691,6 @@ describe('helpers', () => {
await queryTimelineById(args);
});
expect(dispatchUpdateIsLoading).toBeCalledWith({
id: TimelineId.active,
isLoading: true,
});
// expect(resolveTimeline).toHaveBeenCalled();
const { timeline } = formatTimelineResponseToModel(
omitTypenameInTimeline(getOr({}, 'data.timeline', selectedTimeline)),
@ -741,11 +721,6 @@ describe('helpers', () => {
},
});
});
expect(dispatchUpdateIsLoading).toBeCalledWith({
id: TimelineId.active,
isLoading: false,
});
});
test('should update timeline correctly when timeline is untitled', async () => {
@ -764,11 +739,6 @@ describe('helpers', () => {
queryTimelineById(newArgs);
});
expect(dispatchUpdateIsLoading).toHaveBeenCalledWith({
id: TimelineId.active,
isLoading: true,
});
expect(mockUpdateTimeline).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
@ -778,10 +748,6 @@ describe('helpers', () => {
}),
})
);
expect(dispatchUpdateIsLoading).toHaveBeenCalledWith({
id: TimelineId.active,
isLoading: false,
});
});
test('should update timeline correctly when timeline is already saved and onOpenTimeline is not provided', async () => {
@ -791,11 +757,6 @@ describe('helpers', () => {
queryTimelineById(args);
});
expect(dispatchUpdateIsLoading).toHaveBeenCalledWith({
id: TimelineId.active,
isLoading: true,
});
await waitFor(() => {
expect(mockUpdateTimeline).toHaveBeenNthCalledWith(
1,
@ -860,13 +821,6 @@ describe('helpers', () => {
jest.clearAllMocks();
});
test('dispatch updateIsLoading to true', () => {
expect(dispatchUpdateIsLoading).toBeCalledWith({
id: TimelineId.active,
isLoading: true,
});
});
test('get timeline by Id', () => {
expect(resolveTimeline).toHaveBeenCalled();
});
@ -885,13 +839,6 @@ describe('helpers', () => {
},
});
});
test('dispatch updateIsLoading to false', () => {
expect(dispatchUpdateIsLoading).toBeCalledWith({
id: TimelineId.active,
isLoading: false,
});
});
});
});

View file

@ -9,8 +9,6 @@ import { set } from '@kbn/safer-lodash-set/fp';
import { getOr } from 'lodash/fp';
import { v4 as uuidv4 } from 'uuid';
import deepMerge from 'deepmerge';
import { useDispatch } from 'react-redux';
import { useCallback } from 'react';
import { useDiscoverInTimelineContext } from '../../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
import type {
@ -49,7 +47,6 @@ import {
DEFAULT_TO_MOMENT,
} from '../../../common/utils/default_date_settings';
import { resolveTimeline } from '../../containers/api';
import { timelineActions } from '../../store';
export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline';
@ -314,13 +311,6 @@ export interface QueryTimelineById {
export const useQueryTimelineById = () => {
const { resetDiscoverAppState } = useDiscoverInTimelineContext();
const updateTimeline = useUpdateTimeline();
const dispatch = useDispatch();
const updateIsLoading = useCallback(
(status: { id: string; isLoading: boolean }) =>
dispatch(timelineActions.updateIsLoading(status)),
[dispatch]
);
return ({
activeTimelineTab = TimelineTabs.query,
@ -333,7 +323,6 @@ export const useQueryTimelineById = () => {
openTimeline = true,
savedSearchId,
}: QueryTimelineById) => {
updateIsLoading({ id: TimelineId.active, isLoading: true });
if (timelineId == null) {
updateTimeline({
id: TimelineId.active,
@ -356,7 +345,6 @@ export const useQueryTimelineById = () => {
},
});
resetDiscoverAppState();
updateIsLoading({ id: TimelineId.active, isLoading: false });
} else {
return Promise.resolve(resolveTimeline(timelineId))
.then((result) => {
@ -409,9 +397,6 @@ export const useQueryTimelineById = () => {
if (onError != null) {
onError(error, timelineId);
}
})
.finally(() => {
updateIsLoading({ id: TimelineId.active, isLoading: false });
});
}
};

View file

@ -109,9 +109,6 @@ export const DataProviders = React.memo<Props>(({ timelineId }) => {
const { browserFields } = useSourcererDataView(SourcererScopeName.timeline);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const isLoading = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).isLoading
);
const dataProviders = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).dataProviders
);
@ -167,7 +164,7 @@ export const DataProviders = React.memo<Props>(({ timelineId }) => {
dataProviders={dataProviders}
/>
) : (
<DroppableWrapper isDropDisabled={isLoading} droppableId={droppableId}>
<DroppableWrapper droppableId={droppableId}>
<Empty browserFields={browserFields} timelineId={timelineId} />
</DroppableWrapper>
)}

View file

@ -44,7 +44,6 @@ interface OwnProps {
kqlQuery: string; // eslint-disable-line react/no-unused-prop-types
isEnabled: boolean;
isExcluded: boolean;
isLoading: boolean;
isOpen: boolean;
onDataProviderEdited?: OnDataProviderEdited;
operator: QueryOperator;
@ -77,7 +76,6 @@ interface GetProviderActionsProps {
field: string;
isEnabled: boolean;
isExcluded: boolean;
isLoading: boolean;
onDataProviderEdited?: OnDataProviderEdited;
onFilterForFieldPresent: () => void;
operator: QueryOperator;
@ -98,7 +96,6 @@ export const getProviderActions = ({
field,
isEnabled,
isExcluded,
isLoading,
operator,
onDataProviderEdited,
onFilterForFieldPresent,
@ -116,28 +113,24 @@ export const getProviderActions = ({
items: [
{
className: EDIT_CLASS_NAME,
disabled: isLoading,
icon: 'pencil',
name: i18n.EDIT_MENU_ITEM,
panel: 1,
},
{
className: EXCLUDE_CLASS_NAME,
disabled: isLoading,
icon: `${isExcluded ? 'plusInCircle' : 'minusInCircle'}`,
name: isExcluded ? i18n.INCLUDE_DATA_PROVIDER : i18n.EXCLUDE_DATA_PROVIDER,
onClick: toggleExcluded,
},
{
className: ENABLE_CLASS_NAME,
disabled: isLoading,
icon: `${isEnabled ? 'eyeClosed' : 'eye'}`,
name: isEnabled ? i18n.TEMPORARILY_DISABLE_DATA_PROVIDER : i18n.RE_ENABLE_DATA_PROVIDER,
onClick: toggleEnabled,
},
{
className: FILTER_FOR_FIELD_PRESENT_CLASS_NAME,
disabled: isLoading,
icon: 'logstashFilter',
name: i18n.FILTER_FOR_FIELD_PRESENT,
onClick: onFilterForFieldPresent,
@ -145,7 +138,7 @@ export const getProviderActions = ({
timelineType === TimelineTypeEnum.template
? {
className: CONVERT_TO_FIELD_CLASS_NAME,
disabled: isLoading || operator === IS_ONE_OF_OPERATOR,
disabled: operator === IS_ONE_OF_OPERATOR,
icon: 'visText',
name:
type === DataProviderTypeEnum.template
@ -156,7 +149,6 @@ export const getProviderActions = ({
: { name: null },
{
className: DELETE_CLASS_NAME,
disabled: isLoading,
icon: 'trash',
name: i18n.DELETE_DATA_PROVIDER,
onClick: deleteItem,
@ -196,7 +188,6 @@ export class ProviderItemActions extends React.PureComponent<OwnProps> {
field,
isEnabled,
isExcluded,
isLoading,
isOpen,
operator,
providerId,
@ -216,7 +207,6 @@ export class ProviderItemActions extends React.PureComponent<OwnProps> {
field,
isEnabled,
isExcluded,
isLoading,
onDataProviderEdited: this.onDataProviderEdited,
onFilterForFieldPresent: this.onFilterForFieldPresent,
operator,

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { noop } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
@ -15,10 +14,7 @@ import {
TimelineTypeEnum,
} from '../../../../../common/api/timeline';
import type { BrowserFields } from '../../../../common/containers/source';
import {
useDeepEqualSelector,
useShallowEqualSelector,
} from '../../../../common/hooks/use_selector';
import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
import { timelineSelectors } from '../../../store';
import type { PrimitiveOrArrayOfPrimitives } from '../../../../common/lib/kuery';
@ -27,7 +23,6 @@ import { ProviderBadge } from './provider_badge';
import { ProviderItemActions } from './provider_item_actions';
import type { DataProvidersAnd, QueryOperator } from './data_provider';
import { dragAndDropActions } from '../../../../common/store/drag_and_drop';
import { timelineDefaults } from '../../../store/defaults';
interface ProviderItemBadgeProps {
andProviderId?: string;
@ -86,10 +81,6 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
return getTimeline(state, timelineId)?.timelineType ?? TimelineTypeEnum.default;
});
const { isLoading } = useDeepEqualSelector(
(state) => getTimeline(state, timelineId ?? '') ?? timelineDefaults
);
const togglePopover = useCallback(() => {
setIsPopoverOpen(!isPopoverOpen);
}, [isPopoverOpen, setIsPopoverOpen]);
@ -142,7 +133,7 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
const button = useMemo(
() => (
<ProviderBadge
deleteProvider={!isLoading ? deleteProvider : noop}
deleteProvider={deleteProvider}
field={field}
kqlQuery={kqlQuery}
isEnabled={isEnabled}
@ -163,7 +154,6 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
field,
isEnabled,
isExcluded,
isLoading,
kqlQuery,
onToggleTypeProvider,
operator,
@ -186,7 +176,6 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
kqlQuery={kqlQuery}
isEnabled={isEnabled}
isExcluded={isExcluded}
isLoading={isLoading}
isOpen={isPopoverOpen}
onDataProviderEdited={onDataProviderEdited}
operator={operator}

View file

@ -32,7 +32,7 @@ describe('Providers', () => {
beforeEach(() => {
jest.clearAllMocks();
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false });
(useDeepEqualSelector as jest.Mock).mockReturnValue({});
});
describe('rendering', () => {
@ -88,28 +88,6 @@ describe('Providers', () => {
expect(mockOnDataProviderRemoved.mock.calls[0][0].providerId).toEqual('id-Provider 1');
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when the close button is clicked', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const wrapper = mount(
<TestProviders>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
dataProviders={mockDataProviders}
timelineId={TimelineId.test}
/>
</DroppableWrapper>
</TestProviders>
);
wrapper
.find('[data-test-subj="providerBadge"] [data-euiicon-type]')
.first()
.simulate('click');
expect(mockOnDataProviderRemoved).not.toBeCalled();
});
test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => {
const wrapper = mount(
<TestProviders>
@ -132,31 +110,6 @@ describe('Providers', () => {
.simulate('click');
expect(mockOnDataProviderRemoved.mock.calls[0][0].providerId).toEqual('id-Provider 1');
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const wrapper = mount(
<TestProviders>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
dataProviders={mockDataProviders}
timelineId={TimelineId.test}
/>
</DroppableWrapper>
</TestProviders>
);
wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click');
wrapper.update();
wrapper
.find(`[data-test-subj="providerActions"] .${DELETE_CLASS_NAME}`)
.first()
.simulate('click');
expect(mockOnDataProviderRemoved).not.toBeCalled();
});
});
describe('#onToggleDataProviderEnabled', () => {
@ -191,35 +144,6 @@ describe('Providers', () => {
providerId: 'id-Provider 1',
});
});
test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const mockOnToggleDataProviderEnabled = jest.spyOn(
timelineActions,
'updateDataProviderEnabled'
);
const wrapper = mount(
<TestProviders>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
dataProviders={mockDataProviders}
timelineId={TimelineId.test}
/>
</DroppableWrapper>
</TestProviders>
);
wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click');
wrapper.update();
wrapper
.find(`[data-test-subj="providerActions"] .${ENABLE_CLASS_NAME}`)
.first()
.simulate('click');
expect(mockOnToggleDataProviderEnabled).not.toBeCalled();
});
});
describe('#onToggleDataProviderExcluded', () => {
@ -257,37 +181,6 @@ describe('Providers', () => {
providerId: 'id-Provider 1',
});
});
test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const mockOnToggleDataProviderExcluded = jest.spyOn(
timelineActions,
'updateDataProviderExcluded'
);
const wrapper = mount(
<TestProviders>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
dataProviders={mockDataProviders}
timelineId={TimelineId.test}
/>
</DroppableWrapper>
</TestProviders>
);
wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click');
wrapper.update();
wrapper
.find(`[data-test-subj="providerActions"] .${EXCLUDE_CLASS_NAME}`)
.first()
.simulate('click');
expect(mockOnToggleDataProviderExcluded).not.toBeCalled();
});
});
describe('#ProviderWithAndProvider', () => {
@ -349,35 +242,6 @@ describe('Providers', () => {
});
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the close button is clicked', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const wrapper = mount(
<TestProviders>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
dataProviders={mockDataProviders}
timelineId={TimelineId.test}
/>
</DroppableWrapper>
</TestProviders>
);
wrapper
.find('[data-test-subj="providerBadge"]')
.at(4)
.find('[data-euiicon-type]')
.first()
.simulate('click');
wrapper.update();
expect(mockOnDataProviderRemoved).not.toBeCalled();
});
test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
@ -420,44 +284,6 @@ describe('Providers', () => {
});
});
test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderEnabled = jest.spyOn(
timelineActions,
'updateDataProviderEnabled'
);
const wrapper = mount(
<TestProviders>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
dataProviders={dataProviders}
timelineId={TimelineId.test}
/>
</DroppableWrapper>
</TestProviders>
);
wrapper
.find('[data-test-subj="providerBadge"]')
.at(4)
.find('button')
.first()
.simulate('click');
wrapper.update();
wrapper
.find(`[data-test-subj="providerActions"] .${ENABLE_CLASS_NAME}`)
.first()
.simulate('click');
expect(mockOnToggleDataProviderEnabled).not.toBeCalled();
});
test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
@ -499,43 +325,5 @@ describe('Providers', () => {
providerId: 'id-Provider 1',
});
});
test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderExcluded = jest.spyOn(
timelineActions,
'updateDataProviderExcluded'
);
const wrapper = mount(
<TestProviders>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
dataProviders={dataProviders}
timelineId={TimelineId.test}
/>
</DroppableWrapper>
</TestProviders>
);
wrapper
.find('[data-test-subj="providerBadge"]')
.at(4)
.find('button')
.first()
.simulate('click');
wrapper.update();
wrapper
.find(`[data-test-subj="providerActions"] .${EXCLUDE_CLASS_NAME}`)
.first()
.simulate('click');
expect(mockOnToggleDataProviderExcluded).not.toBeCalled();
});
});
});

View file

@ -100,7 +100,6 @@ const StatefulTimelineComponent: React.FC<Props> = ({
'sessionViewConfig',
'initialized',
'show',
'isLoading',
'activeTab',
],
getTimeline(state, timelineId) ?? timelineDefaults

View file

@ -7,9 +7,9 @@
import { EuiFlexGroup } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import type { ConnectedProps } from 'react-redux';
import { connect, useDispatch } from 'react-redux';
import { connect } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { InPortal } from 'react-reverse-portal';
import type { EuiDataGridControlColumn } from '@elastic/eui';
@ -25,7 +25,7 @@ import {
} from '../../../../../flyout/document_details/shared/constants/panel_keys';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { timelineActions, timelineSelectors } from '../../../../store';
import { timelineSelectors } from '../../../../store';
import { useTimelineEvents } from '../../../../containers';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import type { inputsModel, State } from '../../../../../common/store';
@ -66,7 +66,6 @@ export const EqlTabContentComponent: React.FC<Props> = ({
eventIdToNoteIds,
}) => {
const { telemetry } = useKibana().services;
const dispatch = useDispatch();
const { query: eqlQuery = '', ...restEqlOption } = eqlOptions;
const { portalNode: eqlEventsCountPortalNode } = useEqlEventsCountPortal();
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
@ -206,15 +205,6 @@ export const EqlTabContentComponent: React.FC<Props> = ({
[dataLoadingState]
);
useEffect(() => {
dispatch(
timelineActions.updateIsLoading({
id: timelineId,
isLoading: isQueryLoading || loadingSourcerer,
})
);
}, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
const unifiedHeader = useMemo(
() => (
<EuiFlexGroup gutterSize="s" direction="column">

View file

@ -280,15 +280,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
[dataLoadingState]
);
useEffect(() => {
dispatch(
timelineActions.updateIsLoading({
id: timelineId,
isLoading: isQueryLoading || loadingSourcerer,
})
);
}, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
// NOTE: The timeline is blank after browser FORWARD navigation (after using back button to navigate to
// the previous page from the timeline), yet we still see total count. This is because the timeline
// is not getting refreshed when using browser navigation.

View file

@ -191,11 +191,6 @@ export const updateEqlOptions = actionCreator<{
value: string | undefined;
}>('UPDATE_EQL_OPTIONS_TIMELINE');
export const updateIsLoading = actionCreator<{
id: string;
isLoading: boolean;
}>('UPDATE_LOADING');
export const setEventsLoading = actionCreator<{
id: string;
eventIds: string[];

View file

@ -64,7 +64,6 @@ export const timelineDefaults: SubsetTimelineModel &
indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
isSaving: false,
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],

View file

@ -104,7 +104,6 @@ const basicTimeline: TimelineModel = {
indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
isSaving: false,
isSelectAllChecked: false,
itemsPerPage: 25,

View file

@ -131,7 +131,6 @@ export const addTimelineToStore = ({
...timelineById,
[id]: {
...timeline,
isLoading: timelineById[id].isLoading,
initialized: timeline.initialized ?? timelineById[id].initialized,
resolveTimelineConfig,
dateRange:
@ -180,7 +179,6 @@ export const addNewTimeline = ({
savedObjectId: null,
version: null,
isSaving: false,
isLoading: false,
timelineType,
...templateTimelineInfo,
},

View file

@ -295,7 +295,6 @@ describe('Timeline save middleware', () => {
isFavorite: false,
isLive: false,
isSelectAllChecked: false,
isLoading: false,
isSaving: false,
itemsPerPage: 25,
itemsPerPageOptions: [10, 25, 50, 100],

View file

@ -129,7 +129,6 @@ export interface TimelineModel {
selectedEventIds: Record<string, TimelineNonEcsData[]>;
/** If selectAll checkbox in header is checked **/
isSelectAllChecked: boolean;
isLoading: boolean;
selectAll: boolean;
/* discover saved search Id */
savedSearchId: string | null;
@ -190,7 +189,6 @@ export type SubsetTimelineModel = Readonly<
| 'show'
| 'sort'
| 'isSaving'
| 'isLoading'
| 'savedObjectId'
| 'version'
| 'status'

View file

@ -45,7 +45,6 @@ import {
removeColumn,
upsertColumn,
updateColumns,
updateIsLoading,
updateSort,
clearSelected,
setSelected,
@ -409,16 +408,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
.case(updateIsLoading, (state, { id, isLoading }) => ({
...state,
timelineById: {
...state.timelineById,
[id]: {
...state.timelineById[id],
isLoading,
},
},
}))
.case(updateSort, (state, { id, sort }) => ({
...state,
timelineById: updateTableSort({ id, sort, timelineById: state.timelineById }),

View file

@ -117,8 +117,6 @@ export const ALERTS_TABLE_COUNT = `[data-test-subj="toolbar-alerts-count"]`;
export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]';
export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]';
export const TIMELINE_COLLAPSED_ITEMS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]';
export const TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN =

View file

@ -57,7 +57,6 @@ import {
TOOLTIP,
} from '../screens/alerts';
import { LOADING_INDICATOR, REFRESH_BUTTON } from '../screens/security_header';
import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline';
import {
UPDATE_ENRICHMENT_RANGE_BUTTON,
ENRICHMENT_QUERY_END_INPUT,
@ -216,7 +215,6 @@ export const goToClosedAlertsOnRuleDetailsPage = () => {
cy.get(CLOSED_ALERTS_FILTER_BTN).click();
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query');
cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist');
};
export const goToClosedAlerts = () => {
@ -233,7 +231,6 @@ export const goToClosedAlerts = () => {
selectPageFilterValue(0, 'closed');
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query');
cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist');
};
export const goToOpenedAlertsOnRuleDetailsPage = () => {
@ -297,7 +294,6 @@ export const goToAcknowledgedAlerts = () => {
selectPageFilterValue(0, 'acknowledged');
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query');
cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist');
};
export const markAlertsAcknowledged = () => {