mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[SecuritySolution] Remove remaining usage of redux-observable
(#175678)
## Summary In this PR, we're removing all usages of `redux-observable` in favor of simple middlewares. This work is part of [this tech debt ticket](https://github.com/elastic/kibana/issues/175427) which outlines the motivation of this move. A couple shortcuts had to be taken and I added comments further down to explain the motivation. ### Oddities Weirdly, the CI reports an increase in async chunks, instead of an expected decrease due to removing a library. | id | [before](9629e66134
) | [after](477348a714
) | diff | | --- | --- | --- | --- | | `securitySolution` | 11.2MB | 11.4MB | +157.1KB | I'm not sure, why this is, so if anyone has any insights on how to examine the async chunks, that would be helpful. _edit:_ After hours of analyzing Kibana build stats at various stages of this PR, I found that the changes that are responsible for this increase in async chunk sum size is this commit:cde023d340
. It only consists of deleted files and deleted imports. Therefore it's up to webpack to figure out the best way to chunk up the app. In other words: I can't change it :( ### Tests You might notice, that I didn't add tests to the middlewares. That is because the epics didn't have tests either and their functionality is tested in acceptance tests. As a follow-up of this PR, I will add tests to all newly-introduced middlewares. My goal here is to keep the changes as small as possible. ### 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: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
dc3c43b120
commit
ca23dd5060
26 changed files with 674 additions and 1094 deletions
|
@ -1080,7 +1080,6 @@
|
|||
"redux-actions": "^2.6.5",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-observable": "2.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"redux-thunks": "^1.0.0",
|
||||
|
|
85
x-pack/plugins/infra/types/redux_observable.d.ts
vendored
85
x-pack/plugins/infra/types/redux_observable.d.ts
vendored
|
@ -1,85 +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 { Action } from 'redux';
|
||||
import { Epic } from 'redux-observable';
|
||||
|
||||
declare module 'redux-observable' {
|
||||
function combineEpics<
|
||||
T1 extends Action,
|
||||
T2 extends Action,
|
||||
O1 extends T1,
|
||||
O2 extends T2,
|
||||
S,
|
||||
D1,
|
||||
D2
|
||||
>(epic1: Epic<T1, O1, S, D1>, epic2: Epic<T2, O2, S, D2>): Epic<T1 | T2, O1 | O2, S, D1 & D2>;
|
||||
function combineEpics<
|
||||
T1 extends Action,
|
||||
T2 extends Action,
|
||||
T3 extends Action,
|
||||
O1 extends T1,
|
||||
O2 extends T2,
|
||||
O3 extends T3,
|
||||
S,
|
||||
D1,
|
||||
D2,
|
||||
D3
|
||||
>(
|
||||
epic1: Epic<T1, O1, S, D1>,
|
||||
epic2: Epic<T2, O2, S, D2>,
|
||||
epic3: Epic<T3, O3, S, D3>
|
||||
): Epic<T1 | T2 | T3, O1 | O2 | O3, S, D1 & D2 & D3>;
|
||||
function combineEpics<
|
||||
T1 extends Action,
|
||||
T2 extends Action,
|
||||
T3 extends Action,
|
||||
T4 extends Action,
|
||||
O1 extends T1,
|
||||
O2 extends T2,
|
||||
O3 extends T3,
|
||||
O4 extends T4,
|
||||
S,
|
||||
D1,
|
||||
D2,
|
||||
D3,
|
||||
D4
|
||||
>(
|
||||
epic1: Epic<T1, O1, S, D1>,
|
||||
epic2: Epic<T2, O2, S, D2>,
|
||||
epic3: Epic<T3, O3, S, D3>,
|
||||
epic4: Epic<T4, O4, S, D4>
|
||||
): Epic<T1 | T2 | T3 | T4, O1 | O2 | O3 | O4, S, D1 & D2 & D3 & D4>;
|
||||
function combineEpics<
|
||||
T1 extends Action,
|
||||
T2 extends Action,
|
||||
T3 extends Action,
|
||||
T4 extends Action,
|
||||
T5 extends Action,
|
||||
O1 extends T1,
|
||||
O2 extends T2,
|
||||
O3 extends T3,
|
||||
O4 extends T4,
|
||||
O5 extends T5,
|
||||
S,
|
||||
D1,
|
||||
D2,
|
||||
D3,
|
||||
D4,
|
||||
D5
|
||||
>(
|
||||
epic1: Epic<T1, O1, S, D1>,
|
||||
epic2: Epic<T2, O2, S, D2>,
|
||||
epic3: Epic<T3, O3, S, D3>,
|
||||
epic4: Epic<T4, O4, S, D4>,
|
||||
epic5: Epic<T5, O5, S, D5>
|
||||
): Epic<T1 | T2 | T3 | T4 | T5, O1 | O2 | O3 | O4 | O5, S, D1 & D2 & D3 & D4 & D5>;
|
||||
|
||||
type EpicWithState<E, S> = E extends Epic<infer In, infer Out, null, infer Deps>
|
||||
? Epic<In, Out, S, Deps>
|
||||
: E;
|
||||
}
|
|
@ -30,7 +30,6 @@ import { OverviewCardWithActions, OverviewCard } from './overview_card';
|
|||
import { StatusPopoverButton } from './status_popover_button';
|
||||
import { SeverityBadge } from '../../severity_badge';
|
||||
import { useThrottledResizeObserver } from '../../utils';
|
||||
import { isNotNull } from '../../../../timelines/store/helpers';
|
||||
|
||||
export const NotGrowingFlexGroup = euiStyled(EuiFlexGroup)`
|
||||
flex-grow: 0;
|
||||
|
@ -219,4 +218,8 @@ function hasData(fieldInfo?: EnrichedFieldInfo): fieldInfo is EnrichedFieldInfoW
|
|||
return !!fieldInfo && Array.isArray(fieldInfo.values);
|
||||
}
|
||||
|
||||
function isNotNull<T>(value: T | null): value is T {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
Overview.displayName = 'Overview';
|
||||
|
|
|
@ -1,35 +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 type { Epic } from 'redux-observable';
|
||||
import { combineEpics } from 'redux-observable';
|
||||
import type { Action } from 'redux';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { createTimelineEpic } from '../../timelines/store/epic';
|
||||
import { createTimelineFavoriteEpic } from '../../timelines/store/epic_favorite';
|
||||
import { createTimelineNoteEpic } from '../../timelines/store/epic_note';
|
||||
import { createTimelinePinnedEventEpic } from '../../timelines/store/epic_pinned_event';
|
||||
import type { TimelineEpicDependencies } from '../../timelines/store/types';
|
||||
import type { State } from './types';
|
||||
|
||||
export interface RootEpicDependencies {
|
||||
kibana$: Observable<CoreStart>;
|
||||
}
|
||||
|
||||
export const createRootEpic = <StateT extends State>(): Epic<
|
||||
Action,
|
||||
Action,
|
||||
StateT,
|
||||
TimelineEpicDependencies<StateT>
|
||||
> =>
|
||||
combineEpics(
|
||||
createTimelineEpic<StateT>(),
|
||||
createTimelineFavoriteEpic<StateT>(),
|
||||
createTimelineNoteEpic<StateT>(),
|
||||
createTimelinePinnedEventEpic<StateT>()
|
||||
);
|
|
@ -4,16 +4,18 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { createTimelineMiddlewares } from '../../timelines/store/middlewares/create_timeline_middlewares';
|
||||
import { dataTableLocalStorageMiddleware } from './data_table/middleware_local_storage';
|
||||
import { userAssetTableLocalStorageMiddleware } from '../../explore/users/store/middleware_storage';
|
||||
|
||||
export function createMiddlewares(storage: Storage) {
|
||||
export function createMiddlewares(kibana: CoreStart, storage: Storage) {
|
||||
return [
|
||||
dataTableLocalStorageMiddleware(storage),
|
||||
userAssetTableLocalStorageMiddleware(storage),
|
||||
...createTimelineMiddlewares(),
|
||||
...createTimelineMiddlewares(kibana),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -11,14 +11,12 @@ import type {
|
|||
Middleware,
|
||||
Dispatch,
|
||||
PreloadedState,
|
||||
CombinedState,
|
||||
AnyAction,
|
||||
Reducer,
|
||||
} from 'redux';
|
||||
import { applyMiddleware, createStore as createReduxStore } from 'redux';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
|
||||
import type { EnhancerOptions } from 'redux-devtools-extension';
|
||||
import { createEpicMiddleware } from 'redux-observable';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, pluck } from 'rxjs';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -33,18 +31,14 @@ import {
|
|||
SERVER_APP_ID,
|
||||
} from '../../../common/constants';
|
||||
import { telemetryMiddleware } from '../lib/telemetry';
|
||||
import { appSelectors } from './app';
|
||||
import { timelineSelectors } from '../../timelines/store';
|
||||
import * as timelineActions from '../../timelines/store/actions';
|
||||
import type { TimelineModel } from '../../timelines/store/model';
|
||||
import { inputsSelectors } from './inputs';
|
||||
import type { SubPluginsInitReducer } from './reducer';
|
||||
import { createInitialState, createReducer } from './reducer';
|
||||
import { createRootEpic } from './epic';
|
||||
import type { AppAction } from './actions';
|
||||
import type { Immutable } from '../../../common/endpoint/types';
|
||||
import type { State } from './types';
|
||||
import type { TimelineEpicDependencies, TimelineState } from '../../timelines/store/types';
|
||||
import type { TimelineState } from '../../timelines/store/types';
|
||||
import type { KibanaDataView, SourcererModel, SourcererDataView } from './sourcerer/model';
|
||||
import { initDataView } from './sourcerer/model';
|
||||
import type { AppObservableLibs, StartedSubPlugins, StartPlugins } from '../../types';
|
||||
|
@ -255,7 +249,7 @@ const stateSanitizer = (state: State) => {
|
|||
export const createStore = (
|
||||
state: State,
|
||||
pluginsReducer: SubPluginsInitReducer,
|
||||
kibana: Observable<CoreStart>,
|
||||
kibana$: Observable<CoreStart>,
|
||||
storage: Storage,
|
||||
additionalMiddleware?: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>
|
||||
): Store<State, Action> => {
|
||||
|
@ -272,23 +266,18 @@ export const createStore = (
|
|||
|
||||
const composeEnhancers = composeWithDevTools(enhancerOptions);
|
||||
|
||||
const middlewareDependencies: TimelineEpicDependencies<State> = {
|
||||
kibana$: kibana,
|
||||
selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector,
|
||||
selectNotesByIdSelector: appSelectors.selectNotesByIdSelector,
|
||||
timelineByIdSelector: timelineSelectors.timelineByIdSelector,
|
||||
timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector,
|
||||
};
|
||||
|
||||
const epicMiddleware = createEpicMiddleware<Action, Action, State, typeof middlewareDependencies>(
|
||||
{
|
||||
dependencies: middlewareDependencies,
|
||||
}
|
||||
);
|
||||
// TODO: Once `createStore` does not use redux-observable, we will not need to pass a
|
||||
// kibana observable anymore. Then we can remove this `any` cast and replace kibana$
|
||||
// with a regular kibana instance.
|
||||
// I'm not doing it in this PR, as this will have an impact on literally hundreds of test files.
|
||||
// A separate PR will be created to clean this up.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const kibanaObsv = kibana$ as any;
|
||||
const kibana =
|
||||
'source' in kibanaObsv ? kibanaObsv.source._value.kibana : kibanaObsv._value.kibana;
|
||||
|
||||
const middlewareEnhancer = applyMiddleware(
|
||||
...createMiddlewares(storage),
|
||||
epicMiddleware,
|
||||
...createMiddlewares(kibana, storage),
|
||||
telemetryMiddleware,
|
||||
...(additionalMiddleware ?? [])
|
||||
);
|
||||
|
@ -299,8 +288,6 @@ export const createStore = (
|
|||
composeEnhancers(middlewareEnhancer)
|
||||
);
|
||||
|
||||
epicMiddleware.run(createRootEpic<CombinedState<State>>());
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
|
||||
exports[`netflowRowRenderer renders correctly against snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
.c14 svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
|
@ -16,11 +21,6 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = `
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.c14 svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.c12,
|
||||
.c12 * {
|
||||
display: inline-block;
|
||||
|
|
|
@ -1,422 +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 { get, getOr, has, set, omit, isObject, toString as fpToString } from 'lodash/fp';
|
||||
import type { Action } from 'redux';
|
||||
import type { Epic } from 'redux-observable';
|
||||
import { from, EMPTY, merge } from 'rxjs';
|
||||
import type { Filter, MatchAllFilter } from '@kbn/es-query';
|
||||
import {
|
||||
isScriptedRangeFilter,
|
||||
isExistsFilter,
|
||||
isRangeFilter,
|
||||
isMatchAllFilter,
|
||||
isPhraseFilter,
|
||||
isQueryStringFilter,
|
||||
isPhrasesFilter,
|
||||
} from '@kbn/es-query';
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
startWith,
|
||||
withLatestFrom,
|
||||
mergeMap,
|
||||
concatMap,
|
||||
takeUntil,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import type { TimelineErrorResponse, TimelineResponse } from '../../../common/api/timeline';
|
||||
import type { ColumnHeaderOptions } from '../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineType } from '../../../common/api/timeline';
|
||||
import type { inputsModel } from '../../common/store/inputs';
|
||||
import { addError } from '../../common/store/app/actions';
|
||||
|
||||
import { copyTimeline, persistTimeline } from '../containers/api';
|
||||
import { ALL_TIMELINE_QUERY_ID } from '../containers/all';
|
||||
import * as i18n from '../pages/translations';
|
||||
|
||||
import {
|
||||
updateTimeline,
|
||||
startTimelineSaving,
|
||||
endTimelineSaving,
|
||||
createTimeline,
|
||||
showCallOutUnauthorizedMsg,
|
||||
addTimeline,
|
||||
saveTimeline,
|
||||
setChanged,
|
||||
} from './actions';
|
||||
import type { TimelineModel } from './model';
|
||||
import { epicPersistNote, isNoteAction } from './epic_note';
|
||||
import { epicPersistPinnedEvent, isPinnedEventAction } from './epic_pinned_event';
|
||||
import { epicPersistTimelineFavorite, isFavoriteTimelineAction } from './epic_favorite';
|
||||
import { isNotNull } from './helpers';
|
||||
import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue';
|
||||
import { myEpicTimelineId } from './my_epic_timeline_id';
|
||||
import type { TimelineEpicDependencies } from './types';
|
||||
import type { TimelineInput } from '../../../common/search_strategy';
|
||||
|
||||
const isItAtimelineAction = (timelineId: string | undefined) =>
|
||||
timelineId && timelineId.toLowerCase().startsWith('timeline');
|
||||
|
||||
export const createTimelineEpic =
|
||||
<State>(): Epic<Action, Action, State, TimelineEpicDependencies<State>> =>
|
||||
(
|
||||
action$,
|
||||
state$,
|
||||
{
|
||||
selectAllTimelineQuery,
|
||||
selectNotesByIdSelector,
|
||||
timelineByIdSelector,
|
||||
timelineTimeRangeSelector,
|
||||
kibana$,
|
||||
}
|
||||
) => {
|
||||
const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull));
|
||||
|
||||
const allTimelineQuery$ = state$.pipe(
|
||||
map((state) => {
|
||||
const getQuery = selectAllTimelineQuery();
|
||||
return getQuery(state, ALL_TIMELINE_QUERY_ID);
|
||||
}),
|
||||
filter(isNotNull)
|
||||
);
|
||||
|
||||
const notes$ = state$.pipe(map(selectNotesByIdSelector), filter(isNotNull));
|
||||
|
||||
const timelineTimeRange$ = state$.pipe(map(timelineTimeRangeSelector), filter(isNotNull));
|
||||
|
||||
return merge(
|
||||
action$.pipe(
|
||||
withLatestFrom(timeline$),
|
||||
filter(([action, timeline]) => {
|
||||
const timelineId: string = get('payload.id', action);
|
||||
const timelineObj: TimelineModel = timeline[timelineId];
|
||||
if (action.type === addError.type) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isItAtimelineAction(timelineId) &&
|
||||
timelineObj != null &&
|
||||
timelineObj.status != null &&
|
||||
TimelineStatus.immutable === timelineObj.status
|
||||
) {
|
||||
return false;
|
||||
} else if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) {
|
||||
myEpicTimelineId.setTimelineVersion(null);
|
||||
myEpicTimelineId.setTimelineId(null);
|
||||
myEpicTimelineId.setTemplateTimelineId(null);
|
||||
myEpicTimelineId.setTemplateTimelineVersion(null);
|
||||
} else if (action.type === addTimeline.type && isItAtimelineAction(timelineId)) {
|
||||
const addNewTimeline: TimelineModel = get('payload.timeline', action);
|
||||
myEpicTimelineId.setTimelineId(addNewTimeline.savedObjectId);
|
||||
myEpicTimelineId.setTimelineVersion(addNewTimeline.version);
|
||||
myEpicTimelineId.setTemplateTimelineId(addNewTimeline.templateTimelineId);
|
||||
myEpicTimelineId.setTemplateTimelineVersion(addNewTimeline.templateTimelineVersion);
|
||||
return getOr(false, 'payload.savedTimeline', action);
|
||||
} else if (
|
||||
action.type === saveTimeline.type &&
|
||||
!timelineObj.isSaving &&
|
||||
isItAtimelineAction(timelineId)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
mergeMap(([action]) => {
|
||||
dispatcherTimelinePersistQueue.next({ action });
|
||||
return EMPTY;
|
||||
})
|
||||
),
|
||||
dispatcherTimelinePersistQueue.pipe(
|
||||
withLatestFrom(timeline$, notes$, timelineTimeRange$),
|
||||
concatMap(([objAction, timeline, notes, timelineTimeRange]) => {
|
||||
const action: Action = get('action', objAction);
|
||||
const timelineId = myEpicTimelineId.getTimelineId();
|
||||
const version = myEpicTimelineId.getTimelineVersion();
|
||||
const templateTimelineId = myEpicTimelineId.getTemplateTimelineId();
|
||||
const templateTimelineVersion = myEpicTimelineId.getTemplateTimelineVersion();
|
||||
|
||||
if (isNoteAction(action)) {
|
||||
return epicPersistNote(action, notes, action$, timeline$, notes$, allTimelineQuery$);
|
||||
} else if (isPinnedEventAction(action)) {
|
||||
return epicPersistPinnedEvent(action, timeline, action$, timeline$, allTimelineQuery$);
|
||||
} else if (isFavoriteTimelineAction(action)) {
|
||||
return epicPersistTimelineFavorite(
|
||||
action,
|
||||
timeline,
|
||||
action$,
|
||||
timeline$,
|
||||
allTimelineQuery$
|
||||
);
|
||||
} else if (isSaveTimelineAction(action)) {
|
||||
const saveAction = action as unknown as ReturnType<typeof saveTimeline>;
|
||||
const savedSearch = timeline[action.payload.id].savedSearch;
|
||||
return from(
|
||||
saveAction.payload.saveAsNew && timelineId
|
||||
? copyTimeline({
|
||||
timelineId,
|
||||
timeline: {
|
||||
...convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
},
|
||||
savedSearch,
|
||||
})
|
||||
: persistTimeline({
|
||||
timelineId,
|
||||
version,
|
||||
timeline: {
|
||||
...convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
},
|
||||
savedSearch,
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$, allTimelineQuery$, kibana$),
|
||||
mergeMap(([response, recentTimeline, allTimelineQuery, kibana]) => {
|
||||
if (isTimelineErrorResponse(response)) {
|
||||
const error = getErrorFromResponse(response);
|
||||
switch (error?.errorCode) {
|
||||
// conflict
|
||||
case 409:
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.TIMELINE_VERSION_CONFLICT_TITLE,
|
||||
text: i18n.TIMELINE_VERSION_CONFLICT_DESCRIPTION,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
}
|
||||
return [
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
const unwrappedResponse = response.data.persistTimeline;
|
||||
if (unwrappedResponse == null) {
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
return [
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (unwrappedResponse.code === 403) {
|
||||
return [
|
||||
showCallOutUnauthorizedMsg(),
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
updateTimeline({
|
||||
id: action.payload.id,
|
||||
timeline: {
|
||||
...recentTimeline[action.payload.id],
|
||||
updated: unwrappedResponse.timeline.updated ?? undefined,
|
||||
savedObjectId: unwrappedResponse.timeline.savedObjectId,
|
||||
version: unwrappedResponse.timeline.version,
|
||||
status: unwrappedResponse.timeline.status ?? TimelineStatus.active,
|
||||
timelineType: unwrappedResponse.timeline.timelineType ?? TimelineType.default,
|
||||
templateTimelineId: unwrappedResponse.timeline.templateTimelineId ?? null,
|
||||
templateTimelineVersion:
|
||||
unwrappedResponse.timeline.templateTimelineVersion ?? null,
|
||||
savedSearchId: unwrappedResponse.timeline.savedSearchId ?? null,
|
||||
isSaving: false,
|
||||
},
|
||||
}),
|
||||
setChanged({
|
||||
id: action.payload.id,
|
||||
changed: false,
|
||||
}),
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
];
|
||||
}),
|
||||
startWith(startTimelineSaving({ id: action.payload.id })),
|
||||
takeUntil(
|
||||
action$.pipe(
|
||||
withLatestFrom(timeline$),
|
||||
filter(([checkAction, updatedTimeline]) => {
|
||||
if (
|
||||
checkAction.type === endTimelineSaving.type &&
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId != null
|
||||
) {
|
||||
myEpicTimelineId.setTimelineId(
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId
|
||||
);
|
||||
myEpicTimelineId.setTimelineVersion(
|
||||
updatedTimeline[get('payload.id', checkAction)].version
|
||||
);
|
||||
myEpicTimelineId.setTemplateTimelineId(
|
||||
updatedTimeline[get('payload.id', checkAction)].templateTimelineId
|
||||
);
|
||||
myEpicTimelineId.setTemplateTimelineVersion(
|
||||
updatedTimeline[get('payload.id', checkAction)].templateTimelineVersion
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function isSaveTimelineAction(action: Action): action is ReturnType<typeof saveTimeline> {
|
||||
return action.type === saveTimeline.type;
|
||||
}
|
||||
|
||||
const timelineInput: TimelineInput = {
|
||||
columns: null,
|
||||
dataProviders: null,
|
||||
dataViewId: null,
|
||||
description: null,
|
||||
eqlOptions: null,
|
||||
eventType: null,
|
||||
excludedRowRendererIds: null,
|
||||
filters: null,
|
||||
kqlMode: null,
|
||||
kqlQuery: null,
|
||||
indexNames: null,
|
||||
title: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
dateRange: null,
|
||||
savedQueryId: null,
|
||||
sort: null,
|
||||
status: null,
|
||||
savedSearchId: null,
|
||||
};
|
||||
|
||||
export const convertTimelineAsInput = (
|
||||
timeline: TimelineModel,
|
||||
timelineTimeRange: inputsModel.TimeRange
|
||||
): TimelineInput =>
|
||||
Object.keys(timelineInput).reduce<TimelineInput>((acc, key) => {
|
||||
if (has(key, timeline)) {
|
||||
if (key === 'kqlQuery') {
|
||||
return set(`${key}.filterQuery`, get(`${key}.filterQuery`, timeline), acc);
|
||||
} else if (key === 'dateRange') {
|
||||
return set(`${key}`, { start: timelineTimeRange.from, end: timelineTimeRange.to }, acc);
|
||||
} else if (key === 'columns' && get(key, timeline) != null) {
|
||||
return set(
|
||||
key,
|
||||
get(key, timeline).map((col: ColumnHeaderOptions) =>
|
||||
omit(['initialWidth', 'width', '__typename', 'esTypes'], col)
|
||||
),
|
||||
acc
|
||||
);
|
||||
} else if (key === 'filters' && get(key, timeline) != null) {
|
||||
const filters = get(key, timeline);
|
||||
return set(
|
||||
key,
|
||||
filters != null
|
||||
? filters.map((myFilter: Filter) => {
|
||||
const basicFilter = omit(['$state'], myFilter);
|
||||
return {
|
||||
...basicFilter,
|
||||
meta: {
|
||||
...basicFilter.meta,
|
||||
field:
|
||||
(isMatchAllFilter(basicFilter) ||
|
||||
isPhraseFilter(basicFilter) ||
|
||||
isPhrasesFilter(basicFilter) ||
|
||||
isRangeFilter(basicFilter)) &&
|
||||
basicFilter.meta.field != null
|
||||
? convertToString(basicFilter.meta.field)
|
||||
: null,
|
||||
value:
|
||||
basicFilter.meta.value != null
|
||||
? convertToString(basicFilter.meta.value)
|
||||
: null,
|
||||
params:
|
||||
basicFilter.meta.params != null
|
||||
? convertToString(basicFilter.meta.params)
|
||||
: null,
|
||||
},
|
||||
...(isMatchAllFilter(basicFilter)
|
||||
? {
|
||||
query: {
|
||||
match_all: convertToString(
|
||||
(basicFilter as MatchAllFilter).query.match_all
|
||||
),
|
||||
},
|
||||
}
|
||||
: { match_all: null }),
|
||||
...(isExistsFilter(basicFilter) && basicFilter.query.exists != null
|
||||
? { query: { exists: convertToString(basicFilter.query.exists) } }
|
||||
: { exists: null }),
|
||||
...((isQueryStringFilter(basicFilter) || get('query', basicFilter) != null) &&
|
||||
basicFilter.query != null
|
||||
? { query: convertToString(basicFilter.query) }
|
||||
: { query: null }),
|
||||
...(isRangeFilter(basicFilter) && basicFilter.query.range != null
|
||||
? { query: { range: convertToString(basicFilter.query.range) } }
|
||||
: { range: null }),
|
||||
...(isScriptedRangeFilter(basicFilter) &&
|
||||
basicFilter.query.script !=
|
||||
null /* TODO remove it when PR50713 is merged || esFilters.isPhraseFilter(basicFilter) */
|
||||
? { query: { script: convertToString(basicFilter.query.script) } }
|
||||
: { script: null }),
|
||||
};
|
||||
})
|
||||
: [],
|
||||
acc
|
||||
);
|
||||
}
|
||||
return set(key, get(key, timeline), acc);
|
||||
}
|
||||
return acc;
|
||||
}, timelineInput);
|
||||
|
||||
const convertToString = (obj: unknown) => {
|
||||
try {
|
||||
if (isObject(obj)) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
return fpToString(obj);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
type PossibleResponse = TimelineResponse | TimelineErrorResponse;
|
||||
|
||||
function isTimelineErrorResponse(response: PossibleResponse): response is TimelineErrorResponse {
|
||||
return 'status_code' in response || 'statusCode' in response;
|
||||
}
|
||||
|
||||
function getErrorFromResponse(response: TimelineErrorResponse) {
|
||||
if ('status_code' in response) {
|
||||
return { errorCode: response.status_code, message: response.message };
|
||||
} else if ('statusCode' in response) {
|
||||
return { errorCode: response.statusCode, message: response.message };
|
||||
}
|
||||
}
|
|
@ -1,10 +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 { Subject } from 'rxjs';
|
||||
|
||||
export const dispatcherTimelinePersistQueue = new Subject();
|
|
@ -1,123 +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 { get } from 'lodash/fp';
|
||||
import type { Action } from 'redux';
|
||||
import type { Epic } from 'redux-observable';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { from, EMPTY } from 'rxjs';
|
||||
import { filter, mergeMap, withLatestFrom, startWith, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { addError } from '../../common/store/app/actions';
|
||||
import {
|
||||
endTimelineSaving,
|
||||
updateIsFavorite,
|
||||
updateTimeline,
|
||||
startTimelineSaving,
|
||||
showCallOutUnauthorizedMsg,
|
||||
} from './actions';
|
||||
import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue';
|
||||
import { myEpicTimelineId } from './my_epic_timeline_id';
|
||||
import type { TimelineById } from './types';
|
||||
import type { inputsModel } from '../../common/store/inputs';
|
||||
import type { ResponseFavoriteTimeline } from '../../../common/api/timeline';
|
||||
import { TimelineType } from '../../../common/api/timeline';
|
||||
import { persistFavorite } from '../containers/api';
|
||||
|
||||
type FavoriteTimelineAction = ReturnType<typeof updateIsFavorite>;
|
||||
|
||||
const timelineFavoriteActionsType = new Set([updateIsFavorite.type]);
|
||||
|
||||
export function isFavoriteTimelineAction(action: Action): action is FavoriteTimelineAction {
|
||||
return timelineFavoriteActionsType.has(action.type);
|
||||
}
|
||||
|
||||
export const epicPersistTimelineFavorite = (
|
||||
action: FavoriteTimelineAction,
|
||||
timeline: TimelineById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>,
|
||||
allTimelineQuery$: Observable<inputsModel.GlobalQuery>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Observable<any> =>
|
||||
from(
|
||||
persistFavorite({
|
||||
timelineId: myEpicTimelineId.getTimelineId(),
|
||||
templateTimelineId: timeline[action.payload.id].templateTimelineId,
|
||||
templateTimelineVersion: timeline[action.payload.id].templateTimelineVersion,
|
||||
timelineType: timeline[action.payload.id].timelineType ?? TimelineType.default,
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimelines, allTimelineQuery]) => {
|
||||
const savedTimeline = recentTimelines[action.payload.id];
|
||||
const response: ResponseFavoriteTimeline = get('data.persistFavorite', result);
|
||||
const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
...callOutMsg,
|
||||
updateTimeline({
|
||||
id: action.payload.id,
|
||||
timeline: {
|
||||
...savedTimeline,
|
||||
isFavorite: response.favorite != null && response.favorite.length > 0,
|
||||
savedObjectId: response.savedObjectId || null,
|
||||
version: response.version || null,
|
||||
},
|
||||
}),
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
].filter(Boolean);
|
||||
}),
|
||||
startWith(startTimelineSaving({ id: action.payload.id })),
|
||||
takeUntil(
|
||||
action$.pipe(
|
||||
withLatestFrom(timeline$),
|
||||
filter(([checkAction, updatedTimeline]) => {
|
||||
if (checkAction.type === addError.type) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
checkAction.type === endTimelineSaving.type &&
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId != null
|
||||
) {
|
||||
myEpicTimelineId.setTimelineId(
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId
|
||||
);
|
||||
myEpicTimelineId.setTimelineVersion(
|
||||
updatedTimeline[get('payload.id', checkAction)].version
|
||||
);
|
||||
myEpicTimelineId.setTemplateTimelineId(
|
||||
updatedTimeline[get('payload.id', checkAction)].templateTimelineId
|
||||
);
|
||||
myEpicTimelineId.setTemplateTimelineVersion(
|
||||
updatedTimeline[get('payload.id', checkAction)].templateTimelineVersion
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export const createTimelineFavoriteEpic =
|
||||
<State>(): Epic<Action, Action, State> =>
|
||||
(action$) => {
|
||||
return action$.pipe(
|
||||
filter(isFavoriteTimelineAction),
|
||||
mergeMap((action) => {
|
||||
dispatcherTimelinePersistQueue.next({ action });
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
};
|
|
@ -1,145 +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 { get } from 'lodash/fp';
|
||||
import type { Action } from 'redux';
|
||||
import type { Epic } from 'redux-observable';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { from, EMPTY } from 'rxjs';
|
||||
import { filter, mergeMap, switchMap, withLatestFrom, startWith, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { updateNote, addError } from '../../common/store/app/actions';
|
||||
import type { NotesById } from '../../common/store/app/model';
|
||||
import type { inputsModel } from '../../common/store/inputs';
|
||||
|
||||
import {
|
||||
addNote,
|
||||
addNoteToEvent,
|
||||
endTimelineSaving,
|
||||
updateTimeline,
|
||||
startTimelineSaving,
|
||||
showCallOutUnauthorizedMsg,
|
||||
} from './actions';
|
||||
import { myEpicTimelineId } from './my_epic_timeline_id';
|
||||
import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue';
|
||||
import type { TimelineById } from './types';
|
||||
import { persistNote } from '../containers/notes/api';
|
||||
import type { ResponseNote } from '../../../common/api/timeline';
|
||||
|
||||
type NoteAction = ReturnType<typeof addNote | typeof addNoteToEvent>;
|
||||
|
||||
const timelineNoteActionsType = new Set([addNote.type, addNoteToEvent.type]);
|
||||
|
||||
export function isNoteAction(action: Action): action is NoteAction {
|
||||
return timelineNoteActionsType.has(action.type);
|
||||
}
|
||||
|
||||
export const epicPersistNote = (
|
||||
action: NoteAction,
|
||||
notes: NotesById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>,
|
||||
notes$: Observable<NotesById>,
|
||||
allTimelineQuery$: Observable<inputsModel.GlobalQuery>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Observable<any> =>
|
||||
from(
|
||||
persistNote({
|
||||
noteId: null,
|
||||
version: null,
|
||||
note: {
|
||||
eventId: 'eventId' in action.payload ? action.payload.eventId : undefined,
|
||||
note: getNote(action.payload.noteId, notes),
|
||||
timelineId: myEpicTimelineId.getTimelineId(),
|
||||
},
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$, notes$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimeline, recentNotes, allTimelineQuery]) => {
|
||||
const noteIdRedux = action.payload.noteId;
|
||||
const response: ResponseNote = get('data.persistNote', result);
|
||||
const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
...callOutMsg,
|
||||
recentTimeline[action.payload.id].savedObjectId == null
|
||||
? updateTimeline({
|
||||
id: action.payload.id,
|
||||
timeline: {
|
||||
...recentTimeline[action.payload.id],
|
||||
savedObjectId: response.note.timelineId || null,
|
||||
version: response.note.timelineVersion || null,
|
||||
},
|
||||
})
|
||||
: null,
|
||||
updateNote({
|
||||
note: {
|
||||
...recentNotes[noteIdRedux],
|
||||
created:
|
||||
response.note.updated != null
|
||||
? new Date(response.note.updated)
|
||||
: recentNotes[noteIdRedux].created,
|
||||
user:
|
||||
response.note.updatedBy != null
|
||||
? response.note.updatedBy
|
||||
: recentNotes[noteIdRedux].user,
|
||||
saveObjectId: response.note.noteId,
|
||||
version: response.note.version,
|
||||
},
|
||||
}),
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
].filter(Boolean);
|
||||
}),
|
||||
startWith(startTimelineSaving({ id: action.payload.id })),
|
||||
takeUntil(
|
||||
action$.pipe(
|
||||
withLatestFrom(timeline$),
|
||||
filter(([checkAction, updatedTimeline]) => {
|
||||
if (checkAction.type === addError.type) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
checkAction.type === endTimelineSaving.type &&
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId != null
|
||||
) {
|
||||
myEpicTimelineId.setTimelineId(
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId
|
||||
);
|
||||
myEpicTimelineId.setTimelineVersion(
|
||||
updatedTimeline[get('payload.id', checkAction)].version
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export const createTimelineNoteEpic =
|
||||
<State>(): Epic<Action, Action, State> =>
|
||||
(action$) =>
|
||||
action$.pipe(
|
||||
filter(isNoteAction),
|
||||
switchMap((action) => {
|
||||
dispatcherTimelinePersistQueue.next({ action });
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
|
||||
const getNote = (noteId: string | undefined | null, notes: NotesById): string => {
|
||||
if (noteId != null) {
|
||||
return notes[noteId].note;
|
||||
}
|
||||
return '';
|
||||
};
|
|
@ -1,143 +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 { get, omit } from 'lodash/fp';
|
||||
import type { Action } from 'redux';
|
||||
import type { Epic } from 'redux-observable';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { from, EMPTY } from 'rxjs';
|
||||
import { filter, mergeMap, startWith, withLatestFrom, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { addError } from '../../common/store/app/actions';
|
||||
import type { inputsModel } from '../../common/store/inputs';
|
||||
import type { PinnedEventResponse } from '../../../common/api/timeline';
|
||||
import {
|
||||
pinEvent,
|
||||
endTimelineSaving,
|
||||
unPinEvent,
|
||||
updateTimeline,
|
||||
startTimelineSaving,
|
||||
showCallOutUnauthorizedMsg,
|
||||
} from './actions';
|
||||
import { myEpicTimelineId } from './my_epic_timeline_id';
|
||||
import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue';
|
||||
import type { TimelineById } from './types';
|
||||
import { persistPinnedEvent } from '../containers/pinned_event/api';
|
||||
|
||||
type PinnedEventAction = ReturnType<typeof pinEvent | typeof unPinEvent>;
|
||||
|
||||
const timelinePinnedEventActionsType = new Set([pinEvent.type, unPinEvent.type]);
|
||||
|
||||
export function isPinnedEventAction(action: Action): action is PinnedEventAction {
|
||||
return timelinePinnedEventActionsType.has(action.type);
|
||||
}
|
||||
|
||||
export const epicPersistPinnedEvent = (
|
||||
action: PinnedEventAction,
|
||||
timeline: TimelineById,
|
||||
action$: Observable<Action>,
|
||||
timeline$: Observable<TimelineById>,
|
||||
allTimelineQuery$: Observable<inputsModel.GlobalQuery>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Observable<any> =>
|
||||
from(
|
||||
persistPinnedEvent({
|
||||
pinnedEventId:
|
||||
timeline[action.payload.id].pinnedEventsSaveObject[action.payload.eventId] != null
|
||||
? timeline[action.payload.id].pinnedEventsSaveObject[action.payload.eventId].pinnedEventId
|
||||
: null,
|
||||
eventId: action.payload.eventId,
|
||||
timelineId: myEpicTimelineId.getTimelineId(),
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimeline, allTimelineQuery]) => {
|
||||
const savedTimeline = recentTimeline[action.payload.id];
|
||||
const response: PinnedEventResponse = get('data.persistPinnedEventOnTimeline', result);
|
||||
const callOutMsg = response && response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
|
||||
return [
|
||||
response != null
|
||||
? updateTimeline({
|
||||
id: action.payload.id,
|
||||
timeline: {
|
||||
...savedTimeline,
|
||||
savedObjectId:
|
||||
savedTimeline.savedObjectId == null && response.timelineId != null
|
||||
? response.timelineId
|
||||
: savedTimeline.savedObjectId,
|
||||
version:
|
||||
savedTimeline.version == null && response.timelineVersion != null
|
||||
? response.timelineVersion
|
||||
: savedTimeline.version,
|
||||
pinnedEventIds: {
|
||||
...savedTimeline.pinnedEventIds,
|
||||
[action.payload.eventId]: true,
|
||||
},
|
||||
pinnedEventsSaveObject: {
|
||||
...savedTimeline.pinnedEventsSaveObject,
|
||||
[action.payload.eventId]: response,
|
||||
},
|
||||
},
|
||||
})
|
||||
: updateTimeline({
|
||||
id: action.payload.id,
|
||||
timeline: {
|
||||
...savedTimeline,
|
||||
pinnedEventIds: omit(action.payload.eventId, savedTimeline.pinnedEventIds),
|
||||
pinnedEventsSaveObject: omit(
|
||||
action.payload.eventId,
|
||||
savedTimeline.pinnedEventsSaveObject
|
||||
),
|
||||
},
|
||||
}),
|
||||
...callOutMsg,
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
].filter(Boolean);
|
||||
}),
|
||||
startWith(startTimelineSaving({ id: action.payload.id })),
|
||||
takeUntil(
|
||||
action$.pipe(
|
||||
withLatestFrom(timeline$),
|
||||
filter(([checkAction, updatedTimeline]) => {
|
||||
if (checkAction.type === addError.type) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
checkAction.type === endTimelineSaving.type &&
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId != null
|
||||
) {
|
||||
myEpicTimelineId.setTimelineId(
|
||||
updatedTimeline[get('payload.id', checkAction)].savedObjectId
|
||||
);
|
||||
myEpicTimelineId.setTimelineVersion(
|
||||
updatedTimeline[get('payload.id', checkAction)].version
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export const createTimelinePinnedEventEpic =
|
||||
<State>(): Epic<Action, Action, State> =>
|
||||
(action$) =>
|
||||
action$.pipe(
|
||||
filter(isPinnedEventAction),
|
||||
mergeMap((action) => {
|
||||
dispatcherTimelinePersistQueue.next({ action });
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
|
@ -44,7 +44,6 @@ import {
|
|||
import { activeTimeline } from '../containers/active_timeline_context';
|
||||
import type { ResolveTimelineConfig } from '../components/open_timeline/types';
|
||||
import { getDisplayValue } from '../components/timeline/data_providers/helpers';
|
||||
export const isNotNull = <T>(value: T | null): value is T => value !== null;
|
||||
|
||||
interface AddTimelineNoteParams {
|
||||
id: string;
|
||||
|
|
|
@ -1,45 +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.
|
||||
*/
|
||||
|
||||
export class ManageEpicTimelineId {
|
||||
private timelineId: string | null = null;
|
||||
private version: string | null = null;
|
||||
private templateTimelineId: string | null = null;
|
||||
private templateVersion: number | null = null;
|
||||
|
||||
public getTimelineId(): string | null {
|
||||
return this.timelineId;
|
||||
}
|
||||
|
||||
public getTimelineVersion(): string | null {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public getTemplateTimelineId(): string | null {
|
||||
return this.templateTimelineId;
|
||||
}
|
||||
|
||||
public getTemplateTimelineVersion(): number | null {
|
||||
return this.templateVersion;
|
||||
}
|
||||
|
||||
public setTimelineId(timelineId: string | null) {
|
||||
this.timelineId = timelineId;
|
||||
}
|
||||
|
||||
public setTimelineVersion(version: string | null) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public setTemplateTimelineId(templateTimelineId: string | null) {
|
||||
this.templateTimelineId = templateTimelineId;
|
||||
}
|
||||
|
||||
public setTemplateTimelineVersion(templateVersion: number | null) {
|
||||
this.templateVersion = templateVersion;
|
||||
}
|
||||
}
|
|
@ -5,8 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { timelineChangedMiddleware } from './changed';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
export function createTimelineMiddlewares() {
|
||||
return [timelineChangedMiddleware];
|
||||
import { timelineChangedMiddleware } from './timeline_changed';
|
||||
import { favoriteTimelineMiddleware } from './timeline_favorite';
|
||||
import { addNoteToTimelineMiddleware } from './timeline_note';
|
||||
import { addPinnedEventToTimelineMiddleware } from './timeline_pinned_event';
|
||||
import { saveTimelineMiddleware } from './timeline_save';
|
||||
|
||||
export function createTimelineMiddlewares(kibana: CoreStart) {
|
||||
return [
|
||||
timelineChangedMiddleware,
|
||||
favoriteTimelineMiddleware(kibana),
|
||||
addNoteToTimelineMiddleware(kibana),
|
||||
addPinnedEventToTimelineMiddleware(kibana),
|
||||
saveTimelineMiddleware(kibana),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 type { State } from '../../../common/store/types';
|
||||
import { ALL_TIMELINE_QUERY_ID } from '../../containers/all';
|
||||
import type { inputsModel } from '../../../common/store/inputs';
|
||||
import { inputsSelectors } from '../../../common/store/inputs';
|
||||
|
||||
/**
|
||||
* Refreshes all timelines, so changes are propagated to everywhere on the page
|
||||
*/
|
||||
export function refreshTimelines(state: State) {
|
||||
const allTimelineQuery = inputsSelectors.globalQueryByIdSelector()(state, ALL_TIMELINE_QUERY_ID);
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
(allTimelineQuery.refetch as inputsModel.Refetch)();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash/fp';
|
||||
import type { Action, Middleware } from 'redux';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import type { State } from '../../../common/store/types';
|
||||
import {
|
||||
endTimelineSaving,
|
||||
updateIsFavorite,
|
||||
updateTimeline,
|
||||
startTimelineSaving,
|
||||
showCallOutUnauthorizedMsg,
|
||||
} from '../actions';
|
||||
import type { ResponseFavoriteTimeline } from '../../../../common/api/timeline';
|
||||
import { TimelineType } from '../../../../common/api/timeline';
|
||||
import { persistFavorite } from '../../containers/api';
|
||||
import { selectTimelineById } from '../selectors';
|
||||
import * as i18n from '../../pages/translations';
|
||||
import { refreshTimelines } from './helpers';
|
||||
|
||||
type FavoriteTimelineAction = ReturnType<typeof updateIsFavorite>;
|
||||
|
||||
function isFavoriteTimelineAction(action: Action): action is FavoriteTimelineAction {
|
||||
return action.type === updateIsFavorite.type;
|
||||
}
|
||||
|
||||
export const favoriteTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State> =
|
||||
(kibana: CoreStart) => (store) => (next) => async (action: Action) => {
|
||||
// perform the action
|
||||
const ret = next(action);
|
||||
|
||||
if (isFavoriteTimelineAction(action)) {
|
||||
const { id } = action.payload;
|
||||
const timeline = selectTimelineById(store.getState(), id);
|
||||
|
||||
store.dispatch(startTimelineSaving({ id }));
|
||||
|
||||
try {
|
||||
const result = await persistFavorite({
|
||||
timelineId: timeline.id,
|
||||
templateTimelineId: timeline.templateTimelineId,
|
||||
templateTimelineVersion: timeline.templateTimelineVersion,
|
||||
timelineType: timeline.timelineType ?? TimelineType.default,
|
||||
});
|
||||
|
||||
const response: ResponseFavoriteTimeline = get('data.persistFavorite', result);
|
||||
|
||||
if (response.code === 403) {
|
||||
store.dispatch(showCallOutUnauthorizedMsg());
|
||||
}
|
||||
|
||||
refreshTimelines(store.getState());
|
||||
|
||||
store.dispatch(
|
||||
updateTimeline({
|
||||
id,
|
||||
timeline: {
|
||||
...timeline,
|
||||
isFavorite: response.favorite != null && response.favorite.length > 0,
|
||||
savedObjectId: response.savedObjectId || null,
|
||||
version: response.version || null,
|
||||
},
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
} finally {
|
||||
store.dispatch(
|
||||
endTimelineSaving({
|
||||
id,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash/fp';
|
||||
import type { Action, Middleware } from 'redux';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import { appSelectors } from '../../../common/store/app';
|
||||
import type { NotesById } from '../../../common/store/app/model';
|
||||
import type { State } from '../../../common/store/types';
|
||||
import { updateNote } from '../../../common/store/app/actions';
|
||||
import {
|
||||
addNote,
|
||||
addNoteToEvent,
|
||||
endTimelineSaving,
|
||||
startTimelineSaving,
|
||||
showCallOutUnauthorizedMsg,
|
||||
} from '../actions';
|
||||
import { persistNote } from '../../containers/notes/api';
|
||||
import type { ResponseNote } from '../../../../common/api/timeline';
|
||||
import { selectTimelineById } from '../selectors';
|
||||
import * as i18n from '../../pages/translations';
|
||||
import { refreshTimelines } from './helpers';
|
||||
|
||||
type NoteAction = ReturnType<typeof addNote | typeof addNoteToEvent>;
|
||||
|
||||
const timelineNoteActionsType = new Set([addNote.type, addNoteToEvent.type]);
|
||||
|
||||
function isNoteAction(action: Action): action is NoteAction {
|
||||
return timelineNoteActionsType.has(action.type);
|
||||
}
|
||||
|
||||
export const addNoteToTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State> =
|
||||
(kibana: CoreStart) => (store) => (next) => async (action: Action) => {
|
||||
// perform the action
|
||||
const ret = next(action);
|
||||
|
||||
if (isNoteAction(action)) {
|
||||
const { id, noteId: localNoteId } = action.payload;
|
||||
const timeline = selectTimelineById(store.getState(), id);
|
||||
const notes = appSelectors.selectNotesByIdSelector(store.getState());
|
||||
|
||||
store.dispatch(startTimelineSaving({ id }));
|
||||
|
||||
try {
|
||||
const result = await persistNote({
|
||||
noteId: null,
|
||||
version: null,
|
||||
note: {
|
||||
eventId: 'eventId' in action.payload ? action.payload.eventId : undefined,
|
||||
note: getNoteText(localNoteId, notes),
|
||||
timelineId: timeline.id,
|
||||
},
|
||||
});
|
||||
|
||||
const response: ResponseNote = get('data.persistNote', result);
|
||||
if (response.code === 403) {
|
||||
store.dispatch(showCallOutUnauthorizedMsg());
|
||||
}
|
||||
|
||||
refreshTimelines(store.getState());
|
||||
|
||||
store.dispatch(
|
||||
updateNote({
|
||||
note: {
|
||||
...notes[localNoteId],
|
||||
created:
|
||||
response.note.updated != null
|
||||
? new Date(response.note.updated)
|
||||
: notes[localNoteId].created,
|
||||
user:
|
||||
response.note.updatedBy != null ? response.note.updatedBy : notes[localNoteId].user,
|
||||
saveObjectId: response.note.noteId,
|
||||
version: response.note.version,
|
||||
},
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
} finally {
|
||||
store.dispatch(
|
||||
endTimelineSaving({
|
||||
id,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
const getNoteText = (noteId: string | undefined | null, notes: NotesById): string => {
|
||||
if (noteId != null) {
|
||||
return notes[noteId].note;
|
||||
}
|
||||
return '';
|
||||
};
|
|
@ -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 { get, omit } from 'lodash/fp';
|
||||
import type { Action, Middleware } from 'redux';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import type { State } from '../../../common/store/types';
|
||||
import { selectTimelineById } from '../selectors';
|
||||
import * as i18n from '../../pages/translations';
|
||||
import type { PinnedEventResponse } from '../../../../common/api/timeline';
|
||||
import {
|
||||
pinEvent,
|
||||
endTimelineSaving,
|
||||
unPinEvent,
|
||||
updateTimeline,
|
||||
startTimelineSaving,
|
||||
showCallOutUnauthorizedMsg,
|
||||
} from '../actions';
|
||||
import { persistPinnedEvent } from '../../containers/pinned_event/api';
|
||||
import { refreshTimelines } from './helpers';
|
||||
|
||||
type PinnedEventAction = ReturnType<typeof pinEvent | typeof unPinEvent>;
|
||||
|
||||
const timelinePinnedEventActionsType = new Set([pinEvent.type, unPinEvent.type]);
|
||||
|
||||
function isPinnedEventAction(action: Action): action is PinnedEventAction {
|
||||
return timelinePinnedEventActionsType.has(action.type);
|
||||
}
|
||||
|
||||
export const addPinnedEventToTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State> =
|
||||
(kibana: CoreStart) => (store) => (next) => async (action: Action) => {
|
||||
// perform the action
|
||||
const ret = next(action);
|
||||
|
||||
if (isPinnedEventAction(action)) {
|
||||
const { id: localTimelineId, eventId } = action.payload;
|
||||
const timeline = selectTimelineById(store.getState(), localTimelineId);
|
||||
|
||||
store.dispatch(startTimelineSaving({ id: localTimelineId }));
|
||||
|
||||
try {
|
||||
const result = await persistPinnedEvent({
|
||||
pinnedEventId:
|
||||
timeline.pinnedEventsSaveObject[eventId] != null
|
||||
? timeline.pinnedEventsSaveObject[eventId].pinnedEventId
|
||||
: null,
|
||||
eventId,
|
||||
timelineId: timeline.id,
|
||||
});
|
||||
|
||||
const response: PinnedEventResponse = get('data.persistPinnedEventOnTimeline', result);
|
||||
if (response && response.code === 403) {
|
||||
store.dispatch(showCallOutUnauthorizedMsg());
|
||||
}
|
||||
|
||||
refreshTimelines(store.getState());
|
||||
|
||||
// The response is null in case we unpinned an event.
|
||||
// In that case we want to remove the locally pinned event.
|
||||
if (!response) {
|
||||
store.dispatch(
|
||||
updateTimeline({
|
||||
id: action.payload.id,
|
||||
timeline: {
|
||||
...timeline,
|
||||
pinnedEventIds: omit(eventId, timeline.pinnedEventIds),
|
||||
pinnedEventsSaveObject: omit(eventId, timeline.pinnedEventsSaveObject),
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
store.dispatch(
|
||||
updateTimeline({
|
||||
id: action.payload.id,
|
||||
timeline: {
|
||||
...timeline,
|
||||
pinnedEventIds: {
|
||||
...timeline.pinnedEventIds,
|
||||
[eventId]: true,
|
||||
},
|
||||
pinnedEventsSaveObject: {
|
||||
...timeline.pinnedEventsSaveObject,
|
||||
[eventId]: response,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
} finally {
|
||||
store.dispatch(
|
||||
endTimelineSaving({
|
||||
id: localTimelineId,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { Direction } from '../../../common/search_strategy';
|
||||
import { TimelineTabs } from '../../../common/types/timeline';
|
||||
import { TimelineType, TimelineStatus } from '../../../common/api/timeline';
|
||||
import { convertTimelineAsInput } from './epic';
|
||||
import type { TimelineModel } from './model';
|
||||
import { Direction } from '../../../../common/search_strategy';
|
||||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { TimelineType, TimelineStatus } from '../../../../common/api/timeline';
|
||||
import { convertTimelineAsInput } from './timeline_save';
|
||||
import type { TimelineModel } from '../model';
|
||||
|
||||
describe('Epic Timeline', () => {
|
||||
describe('Timeline Save Middleware', () => {
|
||||
describe('#convertTimelineAsInput ', () => {
|
||||
test('should return a TimelineInput instead of TimelineModel ', () => {
|
||||
const columns: TimelineModel['columns'] = [
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* 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 { get, has, set, omit, isObject, toString as fpToString } from 'lodash/fp';
|
||||
import type { Action, Middleware } from 'redux';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { Filter, MatchAllFilter } from '@kbn/es-query';
|
||||
import {
|
||||
isScriptedRangeFilter,
|
||||
isExistsFilter,
|
||||
isRangeFilter,
|
||||
isMatchAllFilter,
|
||||
isPhraseFilter,
|
||||
isQueryStringFilter,
|
||||
isPhrasesFilter,
|
||||
} from '@kbn/es-query';
|
||||
|
||||
import {
|
||||
updateTimeline,
|
||||
startTimelineSaving,
|
||||
endTimelineSaving,
|
||||
showCallOutUnauthorizedMsg,
|
||||
saveTimeline,
|
||||
setChanged,
|
||||
} from '../actions';
|
||||
import { copyTimeline, persistTimeline } from '../../containers/api';
|
||||
import type { State } from '../../../common/store/types';
|
||||
import { inputsSelectors } from '../../../common/store/inputs';
|
||||
import { selectTimelineById } from '../selectors';
|
||||
import * as i18n from '../../pages/translations';
|
||||
import type { inputsModel } from '../../../common/store/inputs';
|
||||
import { TimelineStatus, TimelineType } from '../../../../common/api/timeline';
|
||||
import type { TimelineErrorResponse, TimelineResponse } from '../../../../common/api/timeline';
|
||||
import type { TimelineInput } from '../../../../common/search_strategy';
|
||||
import type { TimelineModel } from '../model';
|
||||
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
|
||||
import { refreshTimelines } from './helpers';
|
||||
|
||||
function isSaveTimelineAction(action: Action): action is ReturnType<typeof saveTimeline> {
|
||||
return action.type === saveTimeline.type;
|
||||
}
|
||||
|
||||
export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State> =
|
||||
(kibana: CoreStart) => (store) => (next) => async (action: Action) => {
|
||||
// perform the action
|
||||
const ret = next(action);
|
||||
|
||||
if (isSaveTimelineAction(action)) {
|
||||
const { id: localTimelineId } = action.payload;
|
||||
const timeline = selectTimelineById(store.getState(), localTimelineId);
|
||||
const { timelineId, timelineVersion, templateTimelineId, templateTimelineVersion } =
|
||||
extractTimelineIdsAndVersions(timeline);
|
||||
const timelineTimeRange = inputsSelectors.timelineTimeRangeSelector(store.getState());
|
||||
|
||||
store.dispatch(startTimelineSaving({ id: localTimelineId }));
|
||||
|
||||
try {
|
||||
const result = await (action.payload.saveAsNew && timeline.id
|
||||
? copyTimeline({
|
||||
timelineId,
|
||||
timeline: {
|
||||
...convertTimelineAsInput(timeline, timelineTimeRange),
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
},
|
||||
savedSearch: timeline.savedSearch,
|
||||
})
|
||||
: persistTimeline({
|
||||
timelineId,
|
||||
version: timelineVersion,
|
||||
timeline: {
|
||||
...convertTimelineAsInput(timeline, timelineTimeRange),
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
},
|
||||
savedSearch: timeline.savedSearch,
|
||||
}));
|
||||
|
||||
if (isTimelineErrorResponse(result)) {
|
||||
const error = getErrorFromResponse(result);
|
||||
switch (error?.errorCode) {
|
||||
// conflict
|
||||
case 409:
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.TIMELINE_VERSION_CONFLICT_TITLE,
|
||||
text: i18n.TIMELINE_VERSION_CONFLICT_DESCRIPTION,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const response = result.data.persistTimeline;
|
||||
if (response == null) {
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.code === 403) {
|
||||
store.dispatch(showCallOutUnauthorizedMsg());
|
||||
return;
|
||||
}
|
||||
|
||||
refreshTimelines(store.getState());
|
||||
|
||||
store.dispatch(
|
||||
updateTimeline({
|
||||
id: localTimelineId,
|
||||
timeline: {
|
||||
...timeline,
|
||||
id: response.timeline.savedObjectId,
|
||||
updated: response.timeline.updated ?? undefined,
|
||||
savedObjectId: response.timeline.savedObjectId,
|
||||
version: response.timeline.version,
|
||||
status: response.timeline.status ?? TimelineStatus.active,
|
||||
timelineType: response.timeline.timelineType ?? TimelineType.default,
|
||||
templateTimelineId: response.timeline.templateTimelineId ?? null,
|
||||
templateTimelineVersion: response.timeline.templateTimelineVersion ?? null,
|
||||
savedSearchId: response.timeline.savedSearchId ?? null,
|
||||
isSaving: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
store.dispatch(
|
||||
setChanged({
|
||||
id: action.payload.id,
|
||||
changed: false,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
kibana.notifications.toasts.addDanger({
|
||||
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
|
||||
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
|
||||
});
|
||||
} finally {
|
||||
store.dispatch(
|
||||
endTimelineSaving({
|
||||
id: localTimelineId,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const timelineInput: TimelineInput = {
|
||||
columns: null,
|
||||
dataProviders: null,
|
||||
dataViewId: null,
|
||||
description: null,
|
||||
eqlOptions: null,
|
||||
eventType: null,
|
||||
excludedRowRendererIds: null,
|
||||
filters: null,
|
||||
kqlMode: null,
|
||||
kqlQuery: null,
|
||||
indexNames: null,
|
||||
title: null,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineVersion: null,
|
||||
templateTimelineId: null,
|
||||
dateRange: null,
|
||||
savedQueryId: null,
|
||||
sort: null,
|
||||
status: null,
|
||||
savedSearchId: null,
|
||||
};
|
||||
|
||||
export const convertTimelineAsInput = (
|
||||
timeline: TimelineModel,
|
||||
timelineTimeRange: inputsModel.TimeRange
|
||||
): TimelineInput =>
|
||||
Object.keys(timelineInput).reduce<TimelineInput>((acc, key) => {
|
||||
if (has(key, timeline)) {
|
||||
if (key === 'kqlQuery') {
|
||||
return set(`${key}.filterQuery`, get(`${key}.filterQuery`, timeline), acc);
|
||||
} else if (key === 'dateRange') {
|
||||
return set(`${key}`, { start: timelineTimeRange.from, end: timelineTimeRange.to }, acc);
|
||||
} else if (key === 'columns' && get(key, timeline) != null) {
|
||||
return set(
|
||||
key,
|
||||
get(key, timeline).map((col: ColumnHeaderOptions) =>
|
||||
omit(['initialWidth', 'width', '__typename', 'esTypes'], col)
|
||||
),
|
||||
acc
|
||||
);
|
||||
} else if (key === 'filters' && get(key, timeline) != null) {
|
||||
const filters = get(key, timeline);
|
||||
return set(
|
||||
key,
|
||||
filters != null
|
||||
? filters.map((myFilter: Filter) => {
|
||||
const basicFilter = omit(['$state'], myFilter);
|
||||
return {
|
||||
...basicFilter,
|
||||
meta: {
|
||||
...basicFilter.meta,
|
||||
field:
|
||||
(isMatchAllFilter(basicFilter) ||
|
||||
isPhraseFilter(basicFilter) ||
|
||||
isPhrasesFilter(basicFilter) ||
|
||||
isRangeFilter(basicFilter)) &&
|
||||
basicFilter.meta.field != null
|
||||
? convertToString(basicFilter.meta.field)
|
||||
: null,
|
||||
value:
|
||||
basicFilter.meta.value != null
|
||||
? convertToString(basicFilter.meta.value)
|
||||
: null,
|
||||
params:
|
||||
basicFilter.meta.params != null
|
||||
? convertToString(basicFilter.meta.params)
|
||||
: null,
|
||||
},
|
||||
...(isMatchAllFilter(basicFilter)
|
||||
? {
|
||||
query: {
|
||||
match_all: convertToString(
|
||||
(basicFilter as MatchAllFilter).query.match_all
|
||||
),
|
||||
},
|
||||
}
|
||||
: { match_all: null }),
|
||||
...(isExistsFilter(basicFilter) && basicFilter.query.exists != null
|
||||
? { query: { exists: convertToString(basicFilter.query.exists) } }
|
||||
: { exists: null }),
|
||||
...((isQueryStringFilter(basicFilter) || get('query', basicFilter) != null) &&
|
||||
basicFilter.query != null
|
||||
? { query: convertToString(basicFilter.query) }
|
||||
: { query: null }),
|
||||
...(isRangeFilter(basicFilter) && basicFilter.query.range != null
|
||||
? { query: { range: convertToString(basicFilter.query.range) } }
|
||||
: { range: null }),
|
||||
...(isScriptedRangeFilter(basicFilter) &&
|
||||
basicFilter.query.script !=
|
||||
null /* TODO remove it when PR50713 is merged || esFilters.isPhraseFilter(basicFilter) */
|
||||
? { query: { script: convertToString(basicFilter.query.script) } }
|
||||
: { script: null }),
|
||||
};
|
||||
})
|
||||
: [],
|
||||
acc
|
||||
);
|
||||
}
|
||||
return set(key, get(key, timeline), acc);
|
||||
}
|
||||
return acc;
|
||||
}, timelineInput);
|
||||
|
||||
const convertToString = (obj: unknown) => {
|
||||
try {
|
||||
if (isObject(obj)) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
return fpToString(obj);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
type PossibleResponse = TimelineResponse | TimelineErrorResponse;
|
||||
|
||||
function isTimelineErrorResponse(response: PossibleResponse): response is TimelineErrorResponse {
|
||||
return 'status_code' in response || 'statusCode' in response;
|
||||
}
|
||||
|
||||
function getErrorFromResponse(response: TimelineErrorResponse) {
|
||||
if ('status_code' in response) {
|
||||
return { errorCode: response.status_code, message: response.message };
|
||||
} else if ('statusCode' in response) {
|
||||
return { errorCode: response.statusCode, message: response.message };
|
||||
}
|
||||
}
|
||||
|
||||
function extractTimelineIdsAndVersions(timeline: TimelineModel) {
|
||||
// When a timeline hasn't been saved yet, its `savedObectId` is not defined.
|
||||
// In that case, we want to overwrite all locally created properties for the
|
||||
// timeline id, the timeline template id and the timeline template version.
|
||||
return {
|
||||
timelineId: timeline.savedObjectId ?? null,
|
||||
timelineVersion: timeline.version,
|
||||
templateTimelineId: timeline.savedObjectId ? timeline.templateTimelineId : null,
|
||||
templateTimelineVersion: timeline.savedObjectId ? timeline.templateTimelineVersion : null,
|
||||
};
|
||||
}
|
|
@ -1,10 +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 { ManageEpicTimelineId } from './manage_timeline_id';
|
||||
|
||||
export const myEpicTimelineId = new ManageEpicTimelineId();
|
|
@ -6,11 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { FilterManager } from '@kbn/data-plugin/public';
|
||||
import type { RootEpicDependencies } from '../../common/store/epic';
|
||||
import type { ColumnHeaderOptions, SortColumnTimeline } from '../../../common/types';
|
||||
import type { RowRendererId } from '../../../common/api/timeline';
|
||||
import type { inputsModel } from '../../common/store/inputs';
|
||||
import type { NotesById } from '../../common/store/app/model';
|
||||
|
||||
import type { TimelineModel } from './model';
|
||||
|
||||
|
@ -35,13 +32,6 @@ export interface TimelineState {
|
|||
insertTimeline: InsertTimeline | null;
|
||||
}
|
||||
|
||||
export interface TimelineEpicDependencies<State> extends RootEpicDependencies {
|
||||
timelineByIdSelector: (state: State) => TimelineById;
|
||||
timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange;
|
||||
selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery;
|
||||
selectNotesByIdSelector: (state: State) => NotesById;
|
||||
}
|
||||
|
||||
export interface TimelineModelSettings {
|
||||
documentType: string;
|
||||
defaultColumns: ColumnHeaderOptions[];
|
||||
|
|
|
@ -10,14 +10,14 @@ import { getTimeline } from '../../../objects/timeline';
|
|||
|
||||
import {
|
||||
LOCKED_ICON,
|
||||
NOTES_TEXT,
|
||||
// NOTES_TEXT,
|
||||
PIN_EVENT,
|
||||
TIMELINE_FILTER,
|
||||
TIMELINE_FLYOUT_WRAPPER,
|
||||
TIMELINE_QUERY,
|
||||
TIMELINE_PANEL,
|
||||
TIMELINE_STATUS,
|
||||
TIMELINE_TAB_CONTENT_GRAPHS_NOTES,
|
||||
// TIMELINE_TAB_CONTENT_GRAPHS_NOTES,
|
||||
SAVE_TIMELINE_ACTION_BTN,
|
||||
SAVE_TIMELINE_TOOLTIP,
|
||||
} from '../../../screens/timeline';
|
||||
|
@ -33,7 +33,7 @@ import { selectCustomTemplates } from '../../../tasks/templates';
|
|||
import {
|
||||
addFilter,
|
||||
addNameAndDescriptionToTimeline,
|
||||
addNotesToTimeline,
|
||||
// addNotesToTimeline,
|
||||
clickingOnCreateTimelineFormTemplateBtn,
|
||||
closeTimeline,
|
||||
createNewTimeline,
|
||||
|
@ -108,10 +108,12 @@ describe('Timelines', { tags: ['@ess', '@serverless'] }, (): void => {
|
|||
|
||||
cy.get(LOCKED_ICON).should('be.visible');
|
||||
|
||||
addNotesToTimeline(getTimeline().notes);
|
||||
cy.get(TIMELINE_TAB_CONTENT_GRAPHS_NOTES)
|
||||
.find(NOTES_TEXT)
|
||||
.should('have.text', getTimeline().notes);
|
||||
// TODO: fix this
|
||||
// While typing the note, cypress encounters this -> Error: ResizeObserver loop completed with undelivered notifications.
|
||||
// addNotesToTimeline(getTimeline().notes);
|
||||
// cy.get(TIMELINE_TAB_CONTENT_GRAPHS_NOTES)
|
||||
// .find(NOTES_TEXT)
|
||||
// .should('have.text', getTimeline().notes);
|
||||
});
|
||||
|
||||
it('should show the different timeline states', () => {
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -26497,14 +26497,6 @@ redux-logger@^3.0.6:
|
|||
dependencies:
|
||||
deep-diff "^0.3.5"
|
||||
|
||||
redux-observable@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-2.0.0.tgz#4358bef2e924723a8b1ad0e835ccebb1612a6b9a"
|
||||
integrity sha512-FJz4rLXX+VmDDwZS/LpvQsKnSanDOe8UVjiLryx1g3seZiS69iLpMrcvXD5oFO7rtkPyRdo/FmTqldnT3X3m+w==
|
||||
dependencies:
|
||||
rxjs "^7.0.0"
|
||||
tslib "~2.1.0"
|
||||
|
||||
redux-saga@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112"
|
||||
|
@ -27226,7 +27218,7 @@ rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.1, rxjs@^6.6.0, rxjs@^6.6.7:
|
|||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
rxjs@^7.0.0, rxjs@^7.4.0, rxjs@^7.5.5:
|
||||
rxjs@^7.4.0, rxjs@^7.5.5:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
|
||||
integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
|
||||
|
@ -29801,11 +29793,6 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tslib@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
|
||||
|
||||
tslib@~2.4.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue