[SIEM] Import timeline fix (#65448)
* fix import timeline and clean up fix unit tests apply failure checker clean up error message fix update template * add unit tests * clean up common libs * rename variables * add unit tests * fix types * Fix imports * rename file * poc * fix unit test * review * cleanup fallback values * cleanup * check if title exists * fix unit test * add unit test * lint error * put the flag for disableTemplate into common * add immutiable * fix unit * check templateTimelineVersion only when update via import * update template timeline via import with response * add template filter * add filter count * add filter numbers * rename * enable pin events and note under active status * disable comment and pinnedEvents for template timelines * add timelineType for openTimeline * enable note icon for template * add timeline type for propertyLeft * fix types * duplicate elastic template * update schema * fix status check * fix import * add templateTimelineType * disable note for immutable timeline * fix unit * fix error message * fix update * fix types * rollback change * rollback change * fix create template timeline * add i18n for error message * fix unit test * fix wording and disable delete btn for immutable timeline * fix unit test provider * fix types * fix toaster * fix notes and pins * add i18n * fix selected items * set disableTemplateto true * move templateInfo to helper * review + imporvement * fix review * fix types * fix types Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com> Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
parent
684289d6e3
commit
f4e7f14ffe
|
@ -158,6 +158,12 @@ export const showAllOthersBucket: string[] = [
|
|||
|
||||
/**
|
||||
* CreateTemplateTimelineBtn
|
||||
* https://github.com/elastic/kibana/pull/66613
|
||||
* Remove the comment here to enable template timeline
|
||||
*/
|
||||
export const disableTemplate = true;
|
||||
export const disableTemplate = false;
|
||||
|
||||
/*
|
||||
* This should be set to true after https://github.com/elastic/kibana/pull/67496 is merged
|
||||
*/
|
||||
export const enableElasticFilter = false;
|
||||
|
|
|
@ -137,11 +137,13 @@ const SavedSortRuntimeType = runtimeTypes.partial({
|
|||
export enum TimelineStatus {
|
||||
active = 'active',
|
||||
draft = 'draft',
|
||||
immutable = 'immutable',
|
||||
}
|
||||
|
||||
export const TimelineStatusLiteralRt = runtimeTypes.union([
|
||||
runtimeTypes.literal(TimelineStatus.active),
|
||||
runtimeTypes.literal(TimelineStatus.draft),
|
||||
runtimeTypes.literal(TimelineStatus.immutable),
|
||||
]);
|
||||
|
||||
const TimelineStatusLiteralWithNullRt = unionWithNullType(TimelineStatusLiteralRt);
|
||||
|
@ -151,6 +153,29 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf<
|
|||
typeof TimelineStatusLiteralWithNullRt
|
||||
>;
|
||||
|
||||
/**
|
||||
* Template timeline type
|
||||
*/
|
||||
|
||||
export enum TemplateTimelineType {
|
||||
elastic = 'elastic',
|
||||
custom = 'custom',
|
||||
}
|
||||
|
||||
export const TemplateTimelineTypeLiteralRt = runtimeTypes.union([
|
||||
runtimeTypes.literal(TemplateTimelineType.elastic),
|
||||
runtimeTypes.literal(TemplateTimelineType.custom),
|
||||
]);
|
||||
|
||||
export const TemplateTimelineTypeLiteralWithNullRt = unionWithNullType(
|
||||
TemplateTimelineTypeLiteralRt
|
||||
);
|
||||
|
||||
export type TemplateTimelineTypeLiteral = runtimeTypes.TypeOf<typeof TemplateTimelineTypeLiteralRt>;
|
||||
export type TemplateTimelineTypeLiteralWithNull = runtimeTypes.TypeOf<
|
||||
typeof TemplateTimelineTypeLiteralWithNullRt
|
||||
>;
|
||||
|
||||
/*
|
||||
* Timeline Types
|
||||
*/
|
||||
|
@ -273,6 +298,13 @@ export const TimelineResponseType = runtimeTypes.type({
|
|||
}),
|
||||
});
|
||||
|
||||
export const TimelineErrorResponseType = runtimeTypes.type({
|
||||
status_code: runtimeTypes.number,
|
||||
message: runtimeTypes.string,
|
||||
});
|
||||
|
||||
export interface TimelineErrorResponse
|
||||
extends runtimeTypes.TypeOf<typeof TimelineErrorResponseType> {}
|
||||
export interface TimelineResponse extends runtimeTypes.TypeOf<typeof TimelineResponseType> {}
|
||||
|
||||
/**
|
||||
|
|
|
@ -215,8 +215,8 @@ describe('alert actions', () => {
|
|||
columnId: '@timestamp',
|
||||
sortDirection: 'desc',
|
||||
},
|
||||
status: TimelineStatus.draft,
|
||||
title: '',
|
||||
status: TimelineStatus.active,
|
||||
title: 'Test rule - Duplicate',
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineId: null,
|
||||
templateTimelineVersion: null,
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../mock';
|
||||
import { createStore, State } from '../../store';
|
||||
|
@ -35,10 +36,22 @@ jest.mock('../../lib/kibana', () => ({
|
|||
describe('AddFilterToGlobalSearchBar Component', () => {
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
mockAddFilters.mockClear();
|
||||
});
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
apolloClientObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../mock';
|
||||
import { createStore } from '../../store/store';
|
||||
|
@ -22,10 +23,22 @@ import { State } from '../../store/types';
|
|||
describe('Error Toast Dispatcher', () => {
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mockGlobalState,
|
||||
apolloClientObservable,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../mock';
|
||||
import { createStore, State } from '../../store';
|
||||
|
@ -36,13 +37,25 @@ describe('Inspect Button', () => {
|
|||
state: state.inputs,
|
||||
};
|
||||
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
describe('Render', () => {
|
||||
beforeEach(() => {
|
||||
const myState = cloneDeep(state);
|
||||
myState.inputs = upsertQuery(newQuery);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
test('Eui Empty Button', () => {
|
||||
const wrapper = mount(
|
||||
|
@ -146,7 +159,13 @@ describe('Inspect Button', () => {
|
|||
response: ['my response'],
|
||||
};
|
||||
myState.inputs = upsertQuery(myQuery);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
test('Open Inspect Modal', () => {
|
||||
const wrapper = mount(
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
mockGlobalState,
|
||||
apolloClientObservable,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../mock';
|
||||
import { State, createStore } from '../../store';
|
||||
|
@ -55,7 +56,13 @@ describe('Stat Items Component', () => {
|
|||
const theme = () => ({ eui: euiDarkVars, darkMode: true });
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
describe.each([
|
||||
[
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
apolloClientObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../mock';
|
||||
import { createUseUiSetting$Mock } from '../../mock/kibana_react';
|
||||
|
@ -81,11 +82,23 @@ describe('SIEM Super Date Picker', () => {
|
|||
describe('#SuperDatePicker', () => {
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
mockUseUiSetting$.mockImplementation((key, defaultValue) => {
|
||||
const useUiSetting$Mock = createUseUiSetting$Mock();
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../mock';
|
||||
import { createKibanaCoreStartMock } from '../../mock/kibana_core';
|
||||
|
@ -156,7 +157,13 @@ const state: State = {
|
|||
};
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
describe('StatefulTopN', () => {
|
||||
// Suppress warnings about "react-beautiful-dnd"
|
||||
|
|
|
@ -9,10 +9,10 @@ import ApolloClient from 'apollo-client';
|
|||
import { ApolloLink } from 'apollo-link';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import introspectionQueryResultData from '../../../graphql/introspection.json';
|
||||
import { AppFrontendLibs } from '../lib';
|
||||
import { getLinks } from './helpers';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
|
||||
export function composeLibs(core: CoreStart): AppFrontendLibs {
|
||||
const cache = new InMemoryCache({
|
||||
|
|
|
@ -13,7 +13,7 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
|
|||
import { StartPlugins } from '../../../types';
|
||||
import { depsStartMock } from './dependencies_start_mock';
|
||||
import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../store/test_utils';
|
||||
import { apolloClientObservable } from '../test_providers';
|
||||
import { apolloClientObservable, kibanaObservable } from '../test_providers';
|
||||
import { createStore, State, substateMiddlewareFactory } from '../../store';
|
||||
import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware';
|
||||
import { AppRootProvider } from './app_root_provider';
|
||||
|
@ -58,14 +58,21 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
|
|||
const middlewareSpy = createSpyMiddleware();
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage, [
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage,
|
||||
[
|
||||
substateMiddlewareFactory(
|
||||
(globalState) => globalState.alertList,
|
||||
alertMiddlewareFactory(coreStart, depsStart)
|
||||
),
|
||||
...managementMiddlewareFactory(coreStart, depsStart),
|
||||
middlewareSpy.actionSpyMiddleware,
|
||||
]);
|
||||
]
|
||||
);
|
||||
|
||||
const MockKibanaContextProvider = createKibanaContextProviderMock();
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
DEFAULT_INDEX_PATTERN,
|
||||
} from '../../../common/constants';
|
||||
import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core';
|
||||
import { StartServices } from '../../types';
|
||||
import { createSecuritySolutionStorageMock } from './mock_local_storage';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -71,6 +72,8 @@ export const createUseUiSetting$Mock = () => {
|
|||
): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()];
|
||||
};
|
||||
|
||||
export const createKibanaObservable$Mock = createKibanaCoreStartMock;
|
||||
|
||||
export const createUseKibanaMock = () => {
|
||||
const core = createKibanaCoreStartMock();
|
||||
const plugins = createKibanaPluginsStartMock();
|
||||
|
@ -90,6 +93,36 @@ export const createUseKibanaMock = () => {
|
|||
return () => ({ services });
|
||||
};
|
||||
|
||||
export const createStartServices = () => {
|
||||
const core = createKibanaCoreStartMock();
|
||||
const plugins = createKibanaPluginsStartMock();
|
||||
const security = {
|
||||
authc: {
|
||||
getCurrentUser: jest.fn(),
|
||||
areAPIKeysEnabled: jest.fn(),
|
||||
},
|
||||
sessionTimeout: {
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
extend: jest.fn(),
|
||||
},
|
||||
license: {
|
||||
isEnabled: jest.fn(),
|
||||
getFeatures: jest.fn(),
|
||||
features$: jest.fn(),
|
||||
},
|
||||
__legacyCompat: { logoutUrl: 'logoutUrl', tenant: 'tenant' },
|
||||
};
|
||||
|
||||
const services = ({
|
||||
...core,
|
||||
...plugins,
|
||||
security,
|
||||
} as unknown) as StartServices;
|
||||
|
||||
return services;
|
||||
};
|
||||
|
||||
export const createWithKibanaMock = () => {
|
||||
const kibana = createUseKibanaMock()();
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ThemeProvider } from 'styled-components';
|
|||
|
||||
import { createStore, State } from '../store';
|
||||
import { mockGlobalState } from './global_state';
|
||||
import { createKibanaContextProviderMock } from './kibana_react';
|
||||
import { createKibanaContextProviderMock, createStartServices } from './kibana_react';
|
||||
import { FieldHook, useForm } from '../../shared_imports';
|
||||
import { SUB_PLUGINS_REDUCER } from './utils';
|
||||
import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage';
|
||||
|
@ -38,6 +38,7 @@ export const apolloClient = new ApolloClient({
|
|||
});
|
||||
|
||||
export const apolloClientObservable = new BehaviorSubject(apolloClient);
|
||||
export const kibanaObservable = new BehaviorSubject(createStartServices());
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock(),
|
||||
|
@ -49,7 +50,13 @@ const { storage } = createSecuritySolutionStorageMock();
|
|||
/** A utility for wrapping children in the providers required to run most tests */
|
||||
const TestProvidersComponent: React.FC<Props> = ({
|
||||
children,
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage),
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
),
|
||||
onDragEnd = jest.fn(),
|
||||
}) => (
|
||||
<I18nProvider>
|
||||
|
@ -69,7 +76,13 @@ export const TestProviders = React.memo(TestProvidersComponent);
|
|||
|
||||
const TestProviderWithoutDragAndDropComponent: React.FC<Props> = ({
|
||||
children,
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage),
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
),
|
||||
}) => (
|
||||
<I18nProvider>
|
||||
<ReduxStoreProvider store={store}>{children}</ReduxStoreProvider>
|
||||
|
|
|
@ -29,6 +29,7 @@ import { AppAction } from './actions';
|
|||
import { Immutable } from '../../../common/endpoint/types';
|
||||
import { State } from './types';
|
||||
import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
import { CoreStart } from '../../../../../../src/core/public';
|
||||
|
||||
type ComposeType = typeof compose;
|
||||
declare global {
|
||||
|
@ -49,6 +50,7 @@ export const createStore = (
|
|||
state: PreloadedState<State>,
|
||||
pluginsReducer: SubPluginsInitReducer,
|
||||
apolloClient: Observable<AppApolloClient>,
|
||||
kibana: Observable<CoreStart>,
|
||||
storage: Storage,
|
||||
additionalMiddleware?: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>
|
||||
): Store<State, Action> => {
|
||||
|
@ -56,6 +58,7 @@ export const createStore = (
|
|||
|
||||
const middlewareDependencies = {
|
||||
apolloClient$: apolloClient,
|
||||
kibana$: kibana,
|
||||
selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector,
|
||||
selectNotesByIdSelector: appSelectors.selectNotesByIdSelector,
|
||||
timelineByIdSelector: timelineSelectors.timelineByIdSelector,
|
||||
|
|
|
@ -19,14 +19,7 @@ import { NetworkPluginState } from '../../network/store';
|
|||
import { EndpointAlertsPluginState } from '../../endpoint_alerts';
|
||||
import { ManagementPluginState } from '../../management';
|
||||
|
||||
/**
|
||||
* The redux `State` type for the Security App.
|
||||
* We use `CombinedState` to wrap our shape because we create our reducer using `combineReducers`.
|
||||
* `combineReducers` returns a type wrapped in `CombinedState`.
|
||||
* `CombinedState` is required for redux to know what keys to make optional when preloaded state into a store.
|
||||
*/
|
||||
export type State = CombinedState<
|
||||
HostsPluginState &
|
||||
export type StoreState = HostsPluginState &
|
||||
NetworkPluginState &
|
||||
TimelinePluginState &
|
||||
EndpointAlertsPluginState &
|
||||
|
@ -34,8 +27,14 @@ export type State = CombinedState<
|
|||
app: AppState;
|
||||
dragAndDrop: DragAndDropState;
|
||||
inputs: InputsState;
|
||||
}
|
||||
>;
|
||||
};
|
||||
/**
|
||||
* The redux `State` type for the Security App.
|
||||
* We use `CombinedState` to wrap our shape because we create our reducer using `combineReducers`.
|
||||
* `combineReducers` returns a type wrapped in `CombinedState`.
|
||||
* `CombinedState` is required for redux to know what keys to make optional when preloaded state into a store.
|
||||
*/
|
||||
export type State = CombinedState<StoreState>;
|
||||
|
||||
export type KueryFilterQueryKind = 'kuery' | 'lucene';
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
SUB_PLUGINS_REDUCER,
|
||||
mockGlobalState,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
|
||||
|
@ -31,7 +32,13 @@ export const alertPageTestRender = () => {
|
|||
* Create a store, with the middleware disabled. We don't want side effects being created by our code in this test.
|
||||
*/
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
const store = createStore(
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
const depsStart = depsStartMock();
|
||||
depsStart.data.ui.SearchBar.mockImplementation(() => <div />);
|
||||
|
|
|
@ -255,6 +255,18 @@
|
|||
"description": "",
|
||||
"type": { "kind": "ENUM", "name": "TimelineType", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "templateTimelineType",
|
||||
"description": "",
|
||||
"type": { "kind": "ENUM", "name": "TemplateTimelineType", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"description": "",
|
||||
"type": { "kind": "ENUM", "name": "TimelineStatus", "ofType": null },
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
|
@ -10405,7 +10417,13 @@
|
|||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{ "name": "active", "description": "", "isDeprecated": false, "deprecationReason": null },
|
||||
{ "name": "draft", "description": "", "isDeprecated": false, "deprecationReason": null }
|
||||
{ "name": "draft", "description": "", "isDeprecated": false, "deprecationReason": null },
|
||||
{
|
||||
"name": "immutable",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
|
@ -10529,6 +10547,24 @@
|
|||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "TemplateTimelineType",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "elastic",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{ "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null }
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "ResponseTimelines",
|
||||
|
@ -10557,6 +10593,46 @@
|
|||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "defaultTimelineCount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "templateTimelineCount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "elasticTemplateTimelineCount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "customTemplateTimelineCount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "favoriteCount",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
|
@ -345,6 +345,7 @@ export enum TlsFields {
|
|||
export enum TimelineStatus {
|
||||
active = 'active',
|
||||
draft = 'draft',
|
||||
immutable = 'immutable',
|
||||
}
|
||||
|
||||
export enum TimelineType {
|
||||
|
@ -359,6 +360,11 @@ export enum SortFieldTimeline {
|
|||
created = 'created',
|
||||
}
|
||||
|
||||
export enum TemplateTimelineType {
|
||||
elastic = 'elastic',
|
||||
custom = 'custom',
|
||||
}
|
||||
|
||||
export enum NetworkDirectionEcs {
|
||||
inbound = 'inbound',
|
||||
outbound = 'outbound',
|
||||
|
@ -2117,6 +2123,16 @@ export interface ResponseTimelines {
|
|||
timeline: (Maybe<TimelineResult>)[];
|
||||
|
||||
totalCount?: Maybe<number>;
|
||||
|
||||
defaultTimelineCount?: Maybe<number>;
|
||||
|
||||
templateTimelineCount?: Maybe<number>;
|
||||
|
||||
elasticTemplateTimelineCount?: Maybe<number>;
|
||||
|
||||
customTemplateTimelineCount?: Maybe<number>;
|
||||
|
||||
favoriteCount?: Maybe<number>;
|
||||
}
|
||||
|
||||
export interface Mutation {
|
||||
|
@ -2254,6 +2270,10 @@ export interface GetAllTimelineQueryArgs {
|
|||
onlyUserFavorite?: Maybe<boolean>;
|
||||
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
|
||||
templateTimelineType?: Maybe<TemplateTimelineType>;
|
||||
|
||||
status?: Maybe<TimelineStatus>;
|
||||
}
|
||||
export interface AuthenticationsSourceArgs {
|
||||
timerange: TimerangeInput;
|
||||
|
@ -4315,6 +4335,8 @@ export namespace GetAllTimeline {
|
|||
sort?: Maybe<SortTimeline>;
|
||||
onlyUserFavorite?: Maybe<boolean>;
|
||||
timelineType?: Maybe<TimelineType>;
|
||||
templateTimelineType?: Maybe<TemplateTimelineType>;
|
||||
status?: Maybe<TimelineStatus>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
|
@ -4328,6 +4350,16 @@ export namespace GetAllTimeline {
|
|||
|
||||
totalCount: Maybe<number>;
|
||||
|
||||
defaultTimelineCount: Maybe<number>;
|
||||
|
||||
templateTimelineCount: Maybe<number>;
|
||||
|
||||
elasticTemplateTimelineCount: Maybe<number>;
|
||||
|
||||
customTemplateTimelineCount: Maybe<number>;
|
||||
|
||||
favoriteCount: Maybe<number>;
|
||||
|
||||
timeline: (Maybe<Timeline>)[];
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
apolloClientObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { createStore, State } from '../../../common/store';
|
||||
|
@ -26,10 +27,22 @@ describe('Authentication Table Component', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
@ -40,11 +41,23 @@ describe('Hosts Table', () => {
|
|||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
TestProviders,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../common/mock';
|
||||
import { SiemNavigation } from '../../common/components/navigation';
|
||||
|
@ -154,7 +155,13 @@ describe('Hosts - rendering', () => {
|
|||
});
|
||||
const myState: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
const myStore = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={myStore}>
|
||||
<Router history={mockHistory}>
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { createStore, State } from '../../../common/store';
|
||||
|
@ -28,10 +29,22 @@ describe('IP Overview Component', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
apolloClientObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { createStore, State } from '../../../common/store';
|
||||
|
@ -25,10 +26,22 @@ describe('KpiNetwork Component', () => {
|
|||
const narrowDateRange = jest.fn();
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { State, createStore } from '../../../common/store';
|
||||
|
@ -28,11 +29,23 @@ describe('NetworkTopNFlow Table Component', () => {
|
|||
const loadPage = jest.fn();
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
@ -31,11 +32,23 @@ describe('NetworkHttp Table Component', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
mockIndexPattern,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
@ -32,10 +33,22 @@ describe('NetworkTopCountries Table Component', () => {
|
|||
const mount = useMountAppended();
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
@ -31,11 +32,23 @@ describe('NetworkTopNFlow Table Component', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
@ -28,11 +29,23 @@ describe('Tls Table Component', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
@ -30,11 +31,23 @@ describe('Users Table Component', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
@ -90,7 +91,6 @@ const getMockProps = (ip: string) => ({
|
|||
|
||||
describe('Ip Details', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeAll(() => {
|
||||
(useWithSource as jest.Mock).mockReturnValue({
|
||||
indicesExist: false,
|
||||
|
@ -107,15 +107,27 @@ describe('Ip Details', () => {
|
|||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete (global as GlobalWithFetch).fetch;
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders', () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
mockGlobalState,
|
||||
apolloClientObservable,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../common/mock';
|
||||
import { State, createStore } from '../../common/store';
|
||||
|
@ -139,7 +140,13 @@ describe('rendering - rendering', () => {
|
|||
});
|
||||
const myState: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
const myStore = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const wrapper = mount(
|
||||
<TestProviders store={myStore}>
|
||||
<Router history={mockHistory}>
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
|
||||
|
@ -95,11 +96,23 @@ describe('OverviewHost', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
const myState = cloneDeep(state);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the expected widget title', () => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
createSecuritySolutionStorageMock,
|
||||
kibanaObservable,
|
||||
} from '../../../common/mock';
|
||||
|
||||
import { OverviewNetwork } from '.';
|
||||
|
@ -86,11 +87,23 @@ describe('OverviewNetwork', () => {
|
|||
const state: State = mockGlobalState;
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
const myState = cloneDeep(state);
|
||||
store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
myState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the expected widget title', () => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { RecentTimelines } from './recent_timelines';
|
|||
import * as i18n from './translations';
|
||||
import { FilterMode } from './types';
|
||||
import { LoadingPlaceholders } from '../loading_placeholders';
|
||||
import { useTimelineStatus } from '../../../timelines/components/open_timeline/use_timeline_status';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
|
@ -83,9 +84,9 @@ const StatefulRecentTimelinesComponent = React.memo<Props>(
|
|||
);
|
||||
|
||||
const { fetchAllTimeline, timelines, loading } = useGetAllTimeline();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
const timelineType = TimelineType.default;
|
||||
const { templateTimelineType, timelineStatus } = useTimelineStatus({ timelineType });
|
||||
useEffect(() => {
|
||||
fetchAllTimeline({
|
||||
pageInfo: {
|
||||
pageIndex: 1,
|
||||
|
@ -97,11 +98,11 @@ const StatefulRecentTimelinesComponent = React.memo<Props>(
|
|||
sortOrder: Direction.desc,
|
||||
},
|
||||
onlyUserFavorite: filterBy === 'favorites',
|
||||
timelineType: TimelineType.default,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[filterBy]
|
||||
);
|
||||
status: timelineStatus,
|
||||
timelineType,
|
||||
templateTimelineType,
|
||||
});
|
||||
}, [fetchAllTimeline, filterBy, timelineStatus, timelineType, templateTimelineType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -23,7 +23,14 @@ import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
|
|||
import { initTelemetry } from './common/lib/telemetry';
|
||||
import { KibanaServices } from './common/lib/kibana/services';
|
||||
import { serviceNowActionType, jiraActionType } from './common/lib/connectors';
|
||||
import { PluginSetup, PluginStart, SetupPlugins, StartPlugins, StartServices } from './types';
|
||||
import {
|
||||
PluginSetup,
|
||||
PluginStart,
|
||||
SetupPlugins,
|
||||
StartPlugins,
|
||||
StartServices,
|
||||
AppObservableLibs,
|
||||
} from './types';
|
||||
import {
|
||||
APP_ID,
|
||||
APP_ICON,
|
||||
|
@ -120,6 +127,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
this.downloadAssets(),
|
||||
this.downloadSubPlugins(),
|
||||
]);
|
||||
|
||||
return renderApp({
|
||||
...composeLibs(coreStart),
|
||||
...params,
|
||||
|
@ -396,8 +404,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
endpointAlertsSubPlugin,
|
||||
managementSubPlugin,
|
||||
} = await this.downloadSubPlugins();
|
||||
|
||||
const libs$ = new BehaviorSubject(composeLibs(coreStart));
|
||||
const { apolloClient } = composeLibs(coreStart);
|
||||
const appLibs: AppObservableLibs = { apolloClient, kibana: coreStart };
|
||||
const libs$ = new BehaviorSubject(appLibs);
|
||||
|
||||
const alertsStart = alertsSubPlugin.start(storage);
|
||||
const hostsStart = hostsSubPlugin.start(storage);
|
||||
|
@ -434,6 +443,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
...managementSubPluginStart.store.reducer,
|
||||
},
|
||||
libs$.pipe(pluck('apolloClient')),
|
||||
libs$.pipe(pluck('kibana')),
|
||||
storage,
|
||||
[
|
||||
...(endpointAlertsStart.store.middleware ?? []),
|
||||
|
|
|
@ -41,6 +41,7 @@ const StatefulFlyoutHeader = React.memo<Props>(
|
|||
notesById,
|
||||
status,
|
||||
timelineId,
|
||||
timelineType,
|
||||
title,
|
||||
toggleLock,
|
||||
updateDescription,
|
||||
|
@ -66,6 +67,7 @@ const StatefulFlyoutHeader = React.memo<Props>(
|
|||
noteIds={noteIds}
|
||||
status={status}
|
||||
timelineId={timelineId}
|
||||
timelineType={timelineType}
|
||||
title={title}
|
||||
toggleLock={toggleLock}
|
||||
updateDescription={updateDescription}
|
||||
|
@ -100,6 +102,7 @@ const makeMapStateToProps = () => {
|
|||
title = '',
|
||||
noteIds = emptyNotesId,
|
||||
status,
|
||||
timelineType,
|
||||
} = timeline;
|
||||
|
||||
const history = emptyHistory; // TODO: get history from store via selector
|
||||
|
@ -116,6 +119,7 @@ const makeMapStateToProps = () => {
|
|||
notesById: getNotesByIds(state),
|
||||
status,
|
||||
title,
|
||||
timelineType,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -4,6 +4,7 @@ exports[`FlyoutHeaderWithCloseButton renders correctly against snapshot 1`] = `
|
|||
<FlyoutHeaderWithCloseButton
|
||||
onClose={[MockFunction]}
|
||||
timelineId="test"
|
||||
timelineType="default"
|
||||
usersViewing={
|
||||
Array [
|
||||
"elastic",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { FlyoutHeaderWithCloseButton } from '.';
|
||||
|
||||
|
@ -40,14 +41,16 @@ jest.mock('../../../../common/lib/kibana', () => {
|
|||
});
|
||||
|
||||
describe('FlyoutHeaderWithCloseButton', () => {
|
||||
const props = {
|
||||
onClose: jest.fn(),
|
||||
timelineId: 'test',
|
||||
timelineType: TimelineType.default,
|
||||
usersViewing: ['elastic'],
|
||||
};
|
||||
test('renders correctly against snapshot', () => {
|
||||
const EmptyComponent = shallow(
|
||||
<TestProviders>
|
||||
<FlyoutHeaderWithCloseButton
|
||||
onClose={jest.fn()}
|
||||
timelineId={'test'}
|
||||
usersViewing={['elastic']}
|
||||
/>
|
||||
<FlyoutHeaderWithCloseButton {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot();
|
||||
|
@ -55,13 +58,13 @@ describe('FlyoutHeaderWithCloseButton', () => {
|
|||
|
||||
test('it should invoke onClose when the close button is clicked', () => {
|
||||
const closeMock = jest.fn();
|
||||
const testProps = {
|
||||
...props,
|
||||
onClose: closeMock,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FlyoutHeaderWithCloseButton
|
||||
onClose={closeMock}
|
||||
timelineId={'test'}
|
||||
usersViewing={['elastic']}
|
||||
/>
|
||||
<FlyoutHeaderWithCloseButton {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="close-timeline"] button').first().simulate('click');
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mockGlobalState,
|
||||
TestProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../common/mock';
|
||||
import { createStore, State } from '../../../common/store';
|
||||
|
@ -62,6 +63,7 @@ describe('Flyout', () => {
|
|||
stateShowIsTrue,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
|
@ -86,6 +88,7 @@ describe('Flyout', () => {
|
|||
stateWithDataProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
|
@ -108,6 +111,7 @@ describe('Flyout', () => {
|
|||
stateWithDataProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
|
@ -142,6 +146,7 @@ describe('Flyout', () => {
|
|||
stateWithDataProviders,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
|
|
|
@ -8,52 +8,39 @@ import { mount, shallow } from 'enzyme';
|
|||
import React from 'react';
|
||||
|
||||
import { AddNote } from '.';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
|
||||
describe('AddNote', () => {
|
||||
const note = 'The contents of a new note';
|
||||
const props = {
|
||||
associateNote: jest.fn(),
|
||||
getNewNoteId: jest.fn(),
|
||||
newNote: note,
|
||||
onCancelAddNote: jest.fn(),
|
||||
updateNewNote: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
status: TimelineStatus.active,
|
||||
};
|
||||
|
||||
test('renders correctly', () => {
|
||||
const wrapper = shallow(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = shallow(<AddNote {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders the Cancel button when onCancelAddNote is provided', () => {
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(<AddNote {...props} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="cancel"]').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('it invokes onCancelAddNote when the Cancel button is clicked', () => {
|
||||
const onCancelAddNote = jest.fn();
|
||||
const testProps = {
|
||||
...props,
|
||||
onCancelAddNote,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={onCancelAddNote}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
wrapper.find('[data-test-subj="cancel"]').first().simulate('click');
|
||||
|
||||
|
@ -62,17 +49,12 @@ describe('AddNote', () => {
|
|||
|
||||
test('it does NOT invoke associateNote when the Cancel button is clicked', () => {
|
||||
const associateNote = jest.fn();
|
||||
const testProps = {
|
||||
...props,
|
||||
associateNote,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={associateNote}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
wrapper.find('[data-test-subj="cancel"]').first().simulate('click');
|
||||
|
||||
|
@ -80,47 +62,29 @@ describe('AddNote', () => {
|
|||
});
|
||||
|
||||
test('it does NOT render the Cancel button when onCancelAddNote is NOT provided', () => {
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const testProps = {
|
||||
...props,
|
||||
onCancelAddNote: undefined,
|
||||
};
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="cancel"]').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('it renders the contents of the note', () => {
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(<AddNote {...props} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-a-note"]').first().text()).toEqual(note);
|
||||
});
|
||||
|
||||
test('it invokes associateNote when the Add Note button is clicked', () => {
|
||||
const associateNote = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={associateNote}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const testProps = {
|
||||
...props,
|
||||
newNote: note,
|
||||
associateNote,
|
||||
};
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
|
||||
|
||||
|
@ -129,17 +93,12 @@ describe('AddNote', () => {
|
|||
|
||||
test('it invokes getNewNoteId when the Add Note button is clicked', () => {
|
||||
const getNewNoteId = jest.fn();
|
||||
const testProps = {
|
||||
...props,
|
||||
getNewNoteId,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={getNewNoteId}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
|
||||
|
||||
|
@ -148,17 +107,12 @@ describe('AddNote', () => {
|
|||
|
||||
test('it invokes updateNewNote when the Add Note button is clicked', () => {
|
||||
const updateNewNote = jest.fn();
|
||||
const testProps = {
|
||||
...props,
|
||||
updateNewNote,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={updateNewNote}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
|
||||
|
||||
|
@ -167,17 +121,11 @@ describe('AddNote', () => {
|
|||
|
||||
test('it invokes updateNote when the Add Note button is clicked', () => {
|
||||
const updateNote = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={note}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
);
|
||||
const testProps = {
|
||||
...props,
|
||||
updateNote,
|
||||
};
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
|
||||
|
||||
|
@ -185,16 +133,11 @@ describe('AddNote', () => {
|
|||
});
|
||||
|
||||
test('it does NOT display the markdown formatting hint when a note has NOT been entered', () => {
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={''}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const testProps = {
|
||||
...props,
|
||||
newNote: '',
|
||||
};
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
|
||||
'visibility',
|
||||
|
@ -203,16 +146,11 @@ describe('AddNote', () => {
|
|||
});
|
||||
|
||||
test('it displays the markdown formatting hint when a note has been entered', () => {
|
||||
const wrapper = mount(
|
||||
<AddNote
|
||||
associateNote={jest.fn()}
|
||||
getNewNoteId={jest.fn()}
|
||||
newNote={'We should see a formatting hint now'}
|
||||
onCancelAddNote={jest.fn()}
|
||||
updateNewNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const testProps = {
|
||||
...props,
|
||||
newNote: 'We should see a formatting hint now',
|
||||
};
|
||||
const wrapper = mount(<AddNote {...testProps} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
|
||||
'visibility',
|
||||
|
|
|
@ -61,7 +61,6 @@ export const AddNote = React.memo<{
|
|||
}),
|
||||
[associateNote, getNewNoteId, newNote, updateNewNote, updateNote]
|
||||
);
|
||||
|
||||
return (
|
||||
<AddNotesContainer alignItems="flexEnd" direction="column" gutterSize="none">
|
||||
<NewNote note={newNote} noteInputHeight={200} updateNewNote={updateNewNote} />
|
||||
|
|
|
@ -21,12 +21,14 @@ import { AddNote } from './add_note';
|
|||
import { columns } from './columns';
|
||||
import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers';
|
||||
import { NOTES_PANEL_WIDTH, NOTES_PANEL_HEIGHT } from '../timeline/properties/notes_size';
|
||||
import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline';
|
||||
|
||||
interface Props {
|
||||
associateNote: AssociateNote;
|
||||
getNotesByIds: (noteIds: string[]) => Note[];
|
||||
getNewNoteId: GetNewNoteId;
|
||||
noteIds: string[];
|
||||
status: TimelineStatusLiteral;
|
||||
updateNote: UpdateNote;
|
||||
}
|
||||
|
||||
|
@ -53,8 +55,9 @@ InMemoryTable.displayName = 'InMemoryTable';
|
|||
|
||||
/** A view for entering and reviewing notes */
|
||||
export const Notes = React.memo<Props>(
|
||||
({ associateNote, getNotesByIds, getNewNoteId, noteIds, updateNote }) => {
|
||||
({ associateNote, getNotesByIds, getNewNoteId, noteIds, status, updateNote }) => {
|
||||
const [newNote, setNewNote] = useState('');
|
||||
const isImmutable = status === TimelineStatus.immutable;
|
||||
|
||||
return (
|
||||
<NotesPanel>
|
||||
|
@ -63,6 +66,7 @@ export const Notes = React.memo<Props>(
|
|||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
{!isImmutable && (
|
||||
<AddNote
|
||||
associateNote={associateNote}
|
||||
getNewNoteId={getNewNoteId}
|
||||
|
@ -70,6 +74,7 @@ export const Notes = React.memo<Props>(
|
|||
updateNewNote={setNewNote}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
<InMemoryTable
|
||||
data-test-subj="notes-table"
|
||||
|
|
|
@ -12,6 +12,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
|
|||
import { Note } from '../../../../common/lib/note';
|
||||
|
||||
import { NoteCards } from '.';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
|
||||
describe('NoteCards', () => {
|
||||
const noteIds = ['abc', 'def'];
|
||||
|
@ -38,18 +39,21 @@ describe('NoteCards', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
associateNote: jest.fn(),
|
||||
getNotesByIds,
|
||||
getNewNoteId: jest.fn(),
|
||||
noteIds,
|
||||
showAddNote: true,
|
||||
status: TimelineStatus.active,
|
||||
toggleShowAddNote: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
};
|
||||
|
||||
test('it renders the notes column when noteIds are specified', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<NoteCards
|
||||
associateNote={jest.fn()}
|
||||
getNotesByIds={getNotesByIds}
|
||||
getNewNoteId={jest.fn()}
|
||||
noteIds={noteIds}
|
||||
showAddNote={true}
|
||||
toggleShowAddNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<NoteCards {...props} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
@ -57,17 +61,10 @@ describe('NoteCards', () => {
|
|||
});
|
||||
|
||||
test('it does NOT render the notes column when noteIds are NOT specified', () => {
|
||||
const testProps = { ...props, noteIds: [] };
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<NoteCards
|
||||
associateNote={jest.fn()}
|
||||
getNotesByIds={getNotesByIds}
|
||||
getNewNoteId={jest.fn()}
|
||||
noteIds={[]}
|
||||
showAddNote={true}
|
||||
toggleShowAddNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<NoteCards {...testProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
@ -77,15 +74,7 @@ describe('NoteCards', () => {
|
|||
test('renders note cards', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<NoteCards
|
||||
associateNote={jest.fn()}
|
||||
getNotesByIds={getNotesByIds}
|
||||
getNewNoteId={jest.fn()}
|
||||
noteIds={noteIds}
|
||||
showAddNote={true}
|
||||
toggleShowAddNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<NoteCards {...props} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
@ -102,15 +91,7 @@ describe('NoteCards', () => {
|
|||
test('it shows controls for adding notes when showAddNote is true', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<NoteCards
|
||||
associateNote={jest.fn()}
|
||||
getNotesByIds={getNotesByIds}
|
||||
getNewNoteId={jest.fn()}
|
||||
noteIds={noteIds}
|
||||
showAddNote={true}
|
||||
toggleShowAddNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<NoteCards {...props} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
@ -118,17 +99,11 @@ describe('NoteCards', () => {
|
|||
});
|
||||
|
||||
test('it does NOT show controls for adding notes when showAddNote is false', () => {
|
||||
const testProps = { ...props, showAddNote: false };
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={theme}>
|
||||
<NoteCards
|
||||
associateNote={jest.fn()}
|
||||
getNotesByIds={getNotesByIds}
|
||||
getNewNoteId={jest.fn()}
|
||||
noteIds={noteIds}
|
||||
showAddNote={false}
|
||||
toggleShowAddNote={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<NoteCards {...testProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Note } from '../../../../common/lib/note';
|
|||
import { AddNote } from '../add_note';
|
||||
import { AssociateNote, GetNewNoteId, UpdateNote } from '../helpers';
|
||||
import { NoteCard } from '../note_card';
|
||||
import { TimelineStatusLiteral } from '../../../../../common/types/timeline';
|
||||
|
||||
const AddNoteContainer = styled.div``;
|
||||
AddNoteContainer.displayName = 'AddNoteContainer';
|
||||
|
@ -49,6 +50,7 @@ interface Props {
|
|||
getNewNoteId: GetNewNoteId;
|
||||
noteIds: string[];
|
||||
showAddNote: boolean;
|
||||
status: TimelineStatusLiteral;
|
||||
toggleShowAddNote: () => void;
|
||||
updateNote: UpdateNote;
|
||||
}
|
||||
|
@ -61,6 +63,7 @@ export const NoteCards = React.memo<Props>(
|
|||
getNewNoteId,
|
||||
noteIds,
|
||||
showAddNote,
|
||||
status,
|
||||
toggleShowAddNote,
|
||||
updateNote,
|
||||
}) => {
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
import { EuiContextMenuPanel, EuiContextMenuItem, EuiBasicTable } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
import { TimelineStatus } from '../../../../common/types/timeline';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { DeleteTimelines, OpenTimelineResult } from './types';
|
||||
import { EditTimelineActions } from './export_timeline';
|
||||
|
@ -63,7 +65,7 @@ export const useEditTimelineBatchActions = ({
|
|||
|
||||
const getBatchItemsPopoverContent = useCallback(
|
||||
(closePopover: () => void) => {
|
||||
const isDisabled = isEmpty(selectedItems);
|
||||
const disabled = selectedItems?.some((item) => item.status === TimelineStatus.immutable);
|
||||
return (
|
||||
<>
|
||||
<EditTimelineActions
|
||||
|
@ -82,7 +84,7 @@ export const useEditTimelineBatchActions = ({
|
|||
<EuiContextMenuPanel
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
disabled={isDisabled}
|
||||
disabled={disabled}
|
||||
icon="exportAction"
|
||||
key="ExportItemKey"
|
||||
onClick={handleEnableExportTimelineDownloader}
|
||||
|
@ -90,7 +92,7 @@ export const useEditTimelineBatchActions = ({
|
|||
{i18n.EXPORT_SELECTED}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
disabled={isDisabled}
|
||||
disabled={disabled}
|
||||
icon="trash"
|
||||
key="DeleteItemKey"
|
||||
onClick={handleOnOpenDeleteTimelineModal}
|
||||
|
@ -102,6 +104,7 @@ export const useEditTimelineBatchActions = ({
|
|||
</>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
deleteTimelines,
|
||||
isEnableDownloader,
|
||||
|
|
|
@ -8,7 +8,6 @@ import React from 'react';
|
|||
import { TimelineDownloader } from './export_timeline';
|
||||
import { mockSelectedTimeline } from './mocks';
|
||||
import { ReactWrapper, mount } from 'enzyme';
|
||||
import { useExportTimeline } from '.';
|
||||
|
||||
jest.mock('../translations', () => {
|
||||
return {
|
||||
|
@ -32,19 +31,6 @@ describe('TimelineDownloader', () => {
|
|||
onComplete: jest.fn(),
|
||||
};
|
||||
describe('should not render a downloader', () => {
|
||||
beforeAll(() => {
|
||||
((useExportTimeline as unknown) as jest.Mock).mockReturnValue({
|
||||
enableDownloader: false,
|
||||
setEnableDownloader: jest.fn(),
|
||||
exportedIds: {},
|
||||
getExportedData: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
((useExportTimeline as unknown) as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
test('Without exportedIds', () => {
|
||||
const testProps = {
|
||||
...defaultTestProps,
|
||||
|
@ -65,19 +51,6 @@ describe('TimelineDownloader', () => {
|
|||
});
|
||||
|
||||
describe('should render a downloader', () => {
|
||||
beforeAll(() => {
|
||||
((useExportTimeline as unknown) as jest.Mock).mockReturnValue({
|
||||
enableDownloader: false,
|
||||
setEnableDownloader: jest.fn(),
|
||||
exportedIds: {},
|
||||
getExportedData: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
((useExportTimeline as unknown) as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
test('With selectedItems and exportedIds is given and isEnableDownloader is true', () => {
|
||||
const testProps = {
|
||||
...defaultTestProps,
|
||||
|
|
|
@ -5,31 +5,41 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { useExportTimeline, ExportTimeline } from '.';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EditTimelineActionsComponent } from '.';
|
||||
|
||||
describe('useExportTimeline', () => {
|
||||
describe('call with selected timelines', () => {
|
||||
let exportTimelineRes: ExportTimeline;
|
||||
const TestHook = () => {
|
||||
exportTimelineRes = useExportTimeline();
|
||||
return <div />;
|
||||
describe('EditTimelineActionsComponent', () => {
|
||||
describe('render', () => {
|
||||
const props = {
|
||||
deleteTimelines: jest.fn(),
|
||||
ids: ['id1'],
|
||||
isEnableDownloader: false,
|
||||
isDeleteTimelineModalOpen: false,
|
||||
onComplete: jest.fn(),
|
||||
title: 'mockTitle',
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
mount(<TestHook />);
|
||||
test('should render timelineDownloader', () => {
|
||||
const wrapper = shallow(<EditTimelineActionsComponent {...props} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="TimelineDownloader"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Downloader should be disabled by default', () => {
|
||||
expect(exportTimelineRes.isEnableDownloader).toBeFalsy();
|
||||
test('Should render DeleteTimelineModalOverlay if deleteTimelines is given', () => {
|
||||
const wrapper = shallow(<EditTimelineActionsComponent {...props} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="DeleteTimelineModalOverlay"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Should include disableExportTimelineDownloader in return value', () => {
|
||||
expect(exportTimelineRes).toHaveProperty('disableExportTimelineDownloader');
|
||||
});
|
||||
|
||||
test('Should include enableExportTimelineDownloader in return value', () => {
|
||||
expect(exportTimelineRes).toHaveProperty('enableExportTimelineDownloader');
|
||||
test('Should not render DeleteTimelineModalOverlay if deleteTimelines is not given', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
deleteTimelines: undefined,
|
||||
};
|
||||
const wrapper = shallow(<EditTimelineActionsComponent {...newProps} />);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="DeleteTimelineModalOverlay"]').exists()
|
||||
).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { DeleteTimelines } from '../types';
|
||||
|
||||
import { TimelineDownloader } from './export_timeline';
|
||||
|
@ -17,25 +17,7 @@ export interface ExportTimeline {
|
|||
isEnableDownloader: boolean;
|
||||
}
|
||||
|
||||
export const useExportTimeline = (): ExportTimeline => {
|
||||
const [isEnableDownloader, setIsEnableDownloader] = useState(false);
|
||||
|
||||
const enableExportTimelineDownloader = useCallback(() => {
|
||||
setIsEnableDownloader(true);
|
||||
}, []);
|
||||
|
||||
const disableExportTimelineDownloader = useCallback(() => {
|
||||
setIsEnableDownloader(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
disableExportTimelineDownloader,
|
||||
enableExportTimelineDownloader,
|
||||
isEnableDownloader,
|
||||
};
|
||||
};
|
||||
|
||||
const EditTimelineActionsComponent: React.FC<{
|
||||
export const EditTimelineActionsComponent: React.FC<{
|
||||
deleteTimelines: DeleteTimelines | undefined;
|
||||
ids: string[];
|
||||
isEnableDownloader: boolean;
|
||||
|
@ -52,6 +34,7 @@ const EditTimelineActionsComponent: React.FC<{
|
|||
}) => (
|
||||
<>
|
||||
<TimelineDownloader
|
||||
data-test-subj="TimelineDownloader"
|
||||
exportedIds={ids}
|
||||
getExportedData={exportSelectedTimeline}
|
||||
isEnableDownloader={isEnableDownloader}
|
||||
|
@ -59,6 +42,7 @@ const EditTimelineActionsComponent: React.FC<{
|
|||
/>
|
||||
{deleteTimelines != null && (
|
||||
<DeleteTimelineModalOverlay
|
||||
data-test-subj="DeleteTimelineModalOverlay"
|
||||
deleteTimelines={deleteTimelines}
|
||||
isModalOpen={isDeleteTimelineModalOpen}
|
||||
onComplete={onComplete}
|
||||
|
|
|
@ -10,7 +10,17 @@ import { Action } from 'typescript-fsa';
|
|||
import uuid from 'uuid';
|
||||
import { Dispatch } from 'redux';
|
||||
import { oneTimelineQuery } from '../../containers/one/index.gql_query';
|
||||
import { TimelineResult, GetOneTimeline, NoteResult } from '../../../graphql/types';
|
||||
import {
|
||||
TimelineResult,
|
||||
GetOneTimeline,
|
||||
NoteResult,
|
||||
FilterTimelineResult,
|
||||
ColumnHeaderResult,
|
||||
PinnedEvent,
|
||||
} from '../../../graphql/types';
|
||||
|
||||
import { TimelineStatus, TimelineType } from '../../../../common/types/timeline';
|
||||
|
||||
import {
|
||||
addNotes as dispatchAddNotes,
|
||||
updateNote as dispatchUpdateNote,
|
||||
|
@ -22,9 +32,9 @@ import {
|
|||
addTimeline as dispatchAddTimeline,
|
||||
addNote as dispatchAddGlobalTimelineNote,
|
||||
} from '../../../timelines/store/timeline/actions';
|
||||
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
|
||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
|
||||
import {
|
||||
defaultColumnHeaderType,
|
||||
defaultHeaders,
|
||||
|
@ -77,15 +87,7 @@ const parseString = (params: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const defaultTimelineToTimelineModel = (
|
||||
timeline: TimelineResult,
|
||||
duplicate: boolean
|
||||
): TimelineModel => {
|
||||
return Object.entries({
|
||||
...timeline,
|
||||
columns:
|
||||
timeline.columns != null
|
||||
? timeline.columns.map((col) => {
|
||||
const setTimelineColumn = (col: ColumnHeaderResult) => {
|
||||
const timelineCols: ColumnHeaderOptions = {
|
||||
...col,
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
|
@ -96,40 +98,22 @@ export const defaultTimelineToTimelineModel = (
|
|||
example: col.example != null ? col.example : undefined,
|
||||
type: col.type != null ? col.type : undefined,
|
||||
aggregatable: col.aggregatable != null ? col.aggregatable : undefined,
|
||||
width:
|
||||
col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH,
|
||||
width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH,
|
||||
};
|
||||
return timelineCols;
|
||||
})
|
||||
: defaultHeaders,
|
||||
eventIdToNoteIds: duplicate
|
||||
? {}
|
||||
: timeline.eventIdToNoteIds != null
|
||||
? timeline.eventIdToNoteIds.reduce((acc, note) => {
|
||||
if (note.eventId != null) {
|
||||
const eventNotes = getOr([], note.eventId, acc);
|
||||
return { ...acc, [note.eventId]: [...eventNotes, note.noteId] };
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
: {},
|
||||
filters:
|
||||
timeline.filters != null
|
||||
? timeline.filters.map((filter) => ({
|
||||
};
|
||||
|
||||
const setTimelineFilters = (filter: FilterTimelineResult) => ({
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
meta: {
|
||||
...filter.meta,
|
||||
...(filter.meta && filter.meta.field != null
|
||||
? { params: parseString(filter.meta.field) }
|
||||
: {}),
|
||||
...(filter.meta && filter.meta.field != null ? { params: parseString(filter.meta.field) } : {}),
|
||||
...(filter.meta && filter.meta.params != null
|
||||
? { params: parseString(filter.meta.params) }
|
||||
: {}),
|
||||
...(filter.meta && filter.meta.value != null
|
||||
? { value: parseString(filter.meta.value) }
|
||||
: {}),
|
||||
...(filter.meta && filter.meta.value != null ? { value: parseString(filter.meta.value) } : {}),
|
||||
},
|
||||
...(filter.exists != null ? { exists: parseString(filter.exists) } : {}),
|
||||
...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}),
|
||||
|
@ -137,43 +121,81 @@ export const defaultTimelineToTimelineModel = (
|
|||
...(filter.query != null ? { query: parseString(filter.query) } : {}),
|
||||
...(filter.range != null ? { range: parseString(filter.range) } : {}),
|
||||
...(filter.script != null ? { exists: parseString(filter.script) } : {}),
|
||||
}))
|
||||
: [],
|
||||
isFavorite: duplicate
|
||||
? false
|
||||
: timeline.favorite != null
|
||||
? timeline.favorite.length > 0
|
||||
: false,
|
||||
noteIds: duplicate ? [] : timeline.noteIds != null ? timeline.noteIds : [],
|
||||
pinnedEventIds: duplicate
|
||||
});
|
||||
|
||||
const setEventIdToNoteIds = (
|
||||
duplicate: boolean,
|
||||
eventIdToNoteIds: NoteResult[] | null | undefined
|
||||
) =>
|
||||
duplicate
|
||||
? {}
|
||||
: timeline.pinnedEventIds != null
|
||||
? timeline.pinnedEventIds.reduce(
|
||||
(acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }),
|
||||
{}
|
||||
)
|
||||
: {},
|
||||
pinnedEventsSaveObject: duplicate
|
||||
: eventIdToNoteIds != null
|
||||
? eventIdToNoteIds.reduce((acc, note) => {
|
||||
if (note.eventId != null) {
|
||||
const eventNotes = getOr([], note.eventId, acc);
|
||||
return { ...acc, [note.eventId]: [...eventNotes, note.noteId] };
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
const setPinnedEventsSaveObject = (
|
||||
duplicate: boolean,
|
||||
pinnedEventsSaveObject: PinnedEvent[] | null | undefined
|
||||
) =>
|
||||
duplicate
|
||||
? {}
|
||||
: timeline.pinnedEventsSaveObject != null
|
||||
? timeline.pinnedEventsSaveObject.reduce(
|
||||
: pinnedEventsSaveObject != null
|
||||
? pinnedEventsSaveObject.reduce(
|
||||
(acc, pinnedEvent) => ({
|
||||
...acc,
|
||||
...(pinnedEvent.eventId != null ? { [pinnedEvent.eventId]: pinnedEvent } : {}),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
: {},
|
||||
: {};
|
||||
|
||||
const setPinnedEventIds = (duplicate: boolean, pinnedEventIds: string[] | null | undefined) =>
|
||||
duplicate
|
||||
? {}
|
||||
: pinnedEventIds != null
|
||||
? pinnedEventIds.reduce((acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), {})
|
||||
: {};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export const defaultTimelineToTimelineModel = (
|
||||
timeline: TimelineResult,
|
||||
duplicate: boolean
|
||||
): TimelineModel => {
|
||||
const isTemplate = timeline.timelineType === TimelineType.template;
|
||||
const timelineEntries = {
|
||||
...timeline,
|
||||
columns: timeline.columns != null ? timeline.columns.map(setTimelineColumn) : defaultHeaders,
|
||||
eventIdToNoteIds: setEventIdToNoteIds(duplicate, timeline.eventIdToNoteIds),
|
||||
filters: timeline.filters != null ? timeline.filters.map(setTimelineFilters) : [],
|
||||
isFavorite: duplicate
|
||||
? false
|
||||
: timeline.favorite != null
|
||||
? timeline.favorite.length > 0
|
||||
: false,
|
||||
noteIds: duplicate ? [] : timeline.noteIds != null ? timeline.noteIds : [],
|
||||
pinnedEventIds: setPinnedEventIds(duplicate, timeline.pinnedEventIds),
|
||||
pinnedEventsSaveObject: setPinnedEventsSaveObject(duplicate, timeline.pinnedEventsSaveObject),
|
||||
id: duplicate ? '' : timeline.savedObjectId,
|
||||
status: duplicate ? TimelineStatus.active : timeline.status,
|
||||
savedObjectId: duplicate ? null : timeline.savedObjectId,
|
||||
version: duplicate ? null : timeline.version,
|
||||
title: duplicate ? '' : timeline.title || '',
|
||||
templateTimelineId: duplicate ? null : timeline.templateTimelineId,
|
||||
templateTimelineVersion: duplicate ? null : timeline.templateTimelineVersion,
|
||||
}).reduce((acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc), {
|
||||
title: duplicate ? `${timeline.title} - Duplicate` : timeline.title || '',
|
||||
templateTimelineId: duplicate && isTemplate ? uuid.v4() : timeline.templateTimelineId,
|
||||
templateTimelineVersion: duplicate && isTemplate ? 1 : timeline.templateTimelineVersion,
|
||||
};
|
||||
return Object.entries(timelineEntries).reduce(
|
||||
(acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc),
|
||||
{
|
||||
...timelineDefaults,
|
||||
id: '',
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const formatTimelineResultToModel = (
|
||||
|
|
|
@ -9,9 +9,9 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { Dispatch } from 'redux';
|
||||
import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
|
||||
import { deleteTimelineMutation } from '../../containers/delete/persist.gql_query';
|
||||
import { useGetAllTimeline } from '../../containers/all';
|
||||
|
||||
import { disableTemplate } from '../../../../common/constants';
|
||||
|
||||
import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types';
|
||||
import { State } from '../../../common/store';
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
|
||||
|
@ -21,6 +21,12 @@ import {
|
|||
createTimeline as dispatchCreateNewTimeline,
|
||||
updateIsLoading as dispatchUpdateIsLoading,
|
||||
} from '../../../timelines/store/timeline/actions';
|
||||
|
||||
import { deleteTimelineMutation } from '../../containers/delete/persist.gql_query';
|
||||
import { useGetAllTimeline } from '../../containers/all';
|
||||
|
||||
import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
|
||||
|
||||
import { OpenTimeline } from './open_timeline';
|
||||
import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers';
|
||||
import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body';
|
||||
|
@ -42,7 +48,7 @@ import {
|
|||
} from './types';
|
||||
import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants';
|
||||
import { useTimelineTypes } from './use_timeline_types';
|
||||
import { disableTemplate } from '../../../../common/constants';
|
||||
import { useTimelineStatus } from './use_timeline_status';
|
||||
|
||||
interface OwnProps<TCache = object> {
|
||||
apolloClient: ApolloClient<TCache>;
|
||||
|
@ -106,11 +112,27 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
/** The requested field to sort on */
|
||||
const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD);
|
||||
|
||||
const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes();
|
||||
const { fetchAllTimeline, timelines, loading, totalCount } = useGetAllTimeline();
|
||||
|
||||
const refetch = useCallback(
|
||||
() =>
|
||||
const {
|
||||
customTemplateTimelineCount,
|
||||
defaultTimelineCount,
|
||||
elasticTemplateTimelineCount,
|
||||
favoriteCount,
|
||||
fetchAllTimeline,
|
||||
timelines,
|
||||
loading,
|
||||
totalCount,
|
||||
templateTimelineCount,
|
||||
} = useGetAllTimeline();
|
||||
const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes({
|
||||
defaultTimelineCount,
|
||||
templateTimelineCount,
|
||||
});
|
||||
const { timelineStatus, templateTimelineType, templateTimelineFilter } = useTimelineStatus({
|
||||
timelineType,
|
||||
customTemplateTimelineCount,
|
||||
elasticTemplateTimelineCount,
|
||||
});
|
||||
const refetch = useCallback(() => {
|
||||
fetchAllTimeline({
|
||||
pageInfo: {
|
||||
pageIndex: pageIndex + 1,
|
||||
|
@ -123,11 +145,21 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
},
|
||||
onlyUserFavorite: onlyFavorites,
|
||||
timelineType,
|
||||
}),
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[pageIndex, pageSize, search, sortField, sortDirection, timelineType, onlyFavorites]
|
||||
);
|
||||
templateTimelineType,
|
||||
status: timelineStatus,
|
||||
});
|
||||
}, [
|
||||
fetchAllTimeline,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
search,
|
||||
sortField,
|
||||
sortDirection,
|
||||
timelineType,
|
||||
timelineStatus,
|
||||
templateTimelineType,
|
||||
onlyFavorites,
|
||||
]);
|
||||
|
||||
/** Invoked when the user presses enters to submit the text in the search input */
|
||||
const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => {
|
||||
|
@ -264,6 +296,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
data-test-subj={'open-timeline'}
|
||||
deleteTimelines={onDeleteOneTimeline}
|
||||
defaultPageSize={defaultPageSize}
|
||||
favoriteCount={favoriteCount}
|
||||
isLoading={loading}
|
||||
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
|
||||
importDataModalToggle={importDataModalToggle}
|
||||
|
@ -285,7 +318,9 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
selectedItems={selectedItems}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
tabs={!disableTemplate ? timelineTabs : undefined}
|
||||
templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null}
|
||||
timelineType={timelineType}
|
||||
timelineFilter={!disableTemplate ? timelineTabs : null}
|
||||
title={title}
|
||||
totalSearchResultsCount={totalCount}
|
||||
/>
|
||||
|
@ -294,6 +329,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
data-test-subj={'open-timeline-modal'}
|
||||
deleteTimelines={onDeleteOneTimeline}
|
||||
defaultPageSize={defaultPageSize}
|
||||
favoriteCount={favoriteCount}
|
||||
hideActions={hideActions}
|
||||
isLoading={loading}
|
||||
itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
|
||||
|
@ -312,7 +348,9 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
|
|||
selectedItems={selectedItems}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
tabs={!disableTemplate ? timelineFilters : undefined}
|
||||
templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null}
|
||||
timelineType={timelineType}
|
||||
timelineFilter={!disableTemplate ? timelineFilters : null}
|
||||
title={title}
|
||||
totalSearchResultsCount={totalCount}
|
||||
/>
|
||||
|
|
|
@ -16,6 +16,7 @@ import { TimelinesTableProps } from './timelines_table';
|
|||
import { mockTimelineResults } from '../../../common/mock/timeline_results';
|
||||
import { OpenTimeline } from './open_timeline';
|
||||
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants';
|
||||
import { TimelineType } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
|
@ -46,8 +47,9 @@ describe('OpenTimeline', () => {
|
|||
selectedItems: [],
|
||||
sortDirection: DEFAULT_SORT_DIRECTION,
|
||||
sortField: DEFAULT_SORT_FIELD,
|
||||
tabs: <div />,
|
||||
title,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineFilter: [<div />],
|
||||
totalSearchResultsCount: mockSearchResults.length,
|
||||
});
|
||||
|
||||
|
|
|
@ -4,17 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiPanel, EuiBasicTable } from '@elastic/eui';
|
||||
import { EuiPanel, EuiBasicTable, EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
|
||||
import { OpenTimelineProps, OpenTimelineResult, ActionTimelineToShow } from './types';
|
||||
import { SearchRow } from './search_row';
|
||||
import { TimelinesTable } from './timelines_table';
|
||||
import { ImportDataModal } from '../../../common/components/import_data_modal';
|
||||
import * as i18n from './translations';
|
||||
import { importTimelines } from '../../containers/api';
|
||||
|
||||
import { ImportDataModal } from '../../../common/components/import_data_modal';
|
||||
import {
|
||||
UtilityBarGroup,
|
||||
UtilityBarText,
|
||||
|
@ -22,14 +16,23 @@ import {
|
|||
UtilityBarSection,
|
||||
UtilityBarAction,
|
||||
} from '../../../common/components/utility_bar';
|
||||
|
||||
import { importTimelines } from '../../containers/api';
|
||||
|
||||
import { useEditTimelineBatchActions } from './edit_timeline_batch_actions';
|
||||
import { useEditTimelineActions } from './edit_timeline_actions';
|
||||
import { EditOneTimelineAction } from './export_timeline';
|
||||
import { SearchRow } from './search_row';
|
||||
import { TimelinesTable } from './timelines_table';
|
||||
import * as i18n from './translations';
|
||||
import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
|
||||
import { OpenTimelineProps, OpenTimelineResult, ActionTimelineToShow } from './types';
|
||||
|
||||
export const OpenTimeline = React.memo<OpenTimelineProps>(
|
||||
({
|
||||
deleteTimelines,
|
||||
defaultPageSize,
|
||||
favoriteCount,
|
||||
isLoading,
|
||||
itemIdToExpandedNotesRowMap,
|
||||
importDataModalToggle,
|
||||
|
@ -51,11 +54,12 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
sortDirection,
|
||||
setImportDataModalToggle,
|
||||
sortField,
|
||||
tabs,
|
||||
timelineType,
|
||||
timelineFilter,
|
||||
templateTimelineFilter,
|
||||
totalSearchResultsCount,
|
||||
}) => {
|
||||
const tableRef = useRef<EuiBasicTable<OpenTimelineResult>>();
|
||||
|
||||
const {
|
||||
actionItem,
|
||||
enableExportTimelineDownloader,
|
||||
|
@ -124,6 +128,8 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
[onDeleteSelected, deleteTimelines]
|
||||
);
|
||||
|
||||
const SearchRowContent = useMemo(() => <>{templateTimelineFilter}</>, [templateTimelineFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditOneTimelineAction
|
||||
|
@ -151,15 +157,20 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
/>
|
||||
|
||||
<EuiPanel className={OPEN_TIMELINE_CLASS_NAME}>
|
||||
{!!tabs && tabs}
|
||||
<EuiCallOut size="s" title={i18n.TEMPLATE_CALL_OUT_MESSAGE} />
|
||||
<EuiSpacer size="m" />
|
||||
{!!timelineFilter && timelineFilter}
|
||||
<SearchRow
|
||||
data-test-subj="search-row"
|
||||
favoriteCount={favoriteCount}
|
||||
onlyFavorites={onlyFavorites}
|
||||
onQueryChange={onQueryChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
query={query}
|
||||
totalSearchResultsCount={totalSearchResultsCount}
|
||||
/>
|
||||
>
|
||||
{SearchRowContent}
|
||||
</SearchRow>
|
||||
|
||||
<UtilityBar border>
|
||||
<UtilityBarSection>
|
||||
|
@ -206,6 +217,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
|
|||
showExtendedColumns={true}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
timelineType={timelineType}
|
||||
tableRef={tableRef}
|
||||
totalSearchResultsCount={totalSearchResultsCount}
|
||||
/>
|
||||
|
|
|
@ -16,6 +16,7 @@ import { TimelinesTableProps } from '../timelines_table';
|
|||
import { mockTimelineResults } from '../../../../common/mock/timeline_results';
|
||||
import { OpenTimelineModalBody } from './open_timeline_modal_body';
|
||||
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
|
@ -45,7 +46,8 @@ describe('OpenTimelineModal', () => {
|
|||
selectedItems: [],
|
||||
sortDirection: DEFAULT_SORT_DIRECTION,
|
||||
sortField: DEFAULT_SORT_FIELD,
|
||||
tabs: <div />,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineFilter: [<div />],
|
||||
title,
|
||||
totalSearchResultsCount: mockSearchResults.length,
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
|
|||
({
|
||||
deleteTimelines,
|
||||
defaultPageSize,
|
||||
favoriteCount,
|
||||
hideActions = [],
|
||||
isLoading,
|
||||
itemIdToExpandedNotesRowMap,
|
||||
|
@ -42,7 +43,9 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
|
|||
selectedItems,
|
||||
sortDirection,
|
||||
sortField,
|
||||
tabs,
|
||||
timelineFilter,
|
||||
timelineType,
|
||||
templateTimelineFilter,
|
||||
title,
|
||||
totalSearchResultsCount,
|
||||
}) => {
|
||||
|
@ -54,6 +57,16 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
|
|||
return actions.filter((action) => !hideActions.includes(action));
|
||||
}, [onDeleteSelected, deleteTimelines, hideActions]);
|
||||
|
||||
const SearchRowContent = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{!!timelineFilter && timelineFilter}
|
||||
{!!templateTimelineFilter && templateTimelineFilter}
|
||||
</>
|
||||
),
|
||||
[timelineFilter, templateTimelineFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
|
@ -67,13 +80,15 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
|
|||
<>
|
||||
<SearchRow
|
||||
data-test-subj="search-row"
|
||||
favoriteCount={favoriteCount}
|
||||
onlyFavorites={onlyFavorites}
|
||||
onQueryChange={onQueryChange}
|
||||
onToggleOnlyFavorites={onToggleOnlyFavorites}
|
||||
query={query}
|
||||
tabs={tabs}
|
||||
totalSearchResultsCount={totalSearchResultsCount}
|
||||
/>
|
||||
>
|
||||
{SearchRowContent}
|
||||
</SearchRow>
|
||||
</>
|
||||
</HeaderContainer>
|
||||
</EuiModalHeader>
|
||||
|
@ -96,6 +111,7 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
|
|||
showExtendedColumns={false}
|
||||
sortDirection={sortDirection}
|
||||
sortField={sortField}
|
||||
timelineType={timelineType}
|
||||
totalSearchResultsCount={totalSearchResultsCount}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
|
|
|
@ -34,8 +34,13 @@ SearchRowFlexGroup.displayName = 'SearchRowFlexGroup';
|
|||
|
||||
type Props = Pick<
|
||||
OpenTimelineProps,
|
||||
'onlyFavorites' | 'onQueryChange' | 'onToggleOnlyFavorites' | 'query' | 'totalSearchResultsCount'
|
||||
> & { tabs?: JSX.Element };
|
||||
| 'favoriteCount'
|
||||
| 'onlyFavorites'
|
||||
| 'onQueryChange'
|
||||
| 'onToggleOnlyFavorites'
|
||||
| 'query'
|
||||
| 'totalSearchResultsCount'
|
||||
> & { children?: JSX.Element | null };
|
||||
|
||||
const searchBox = {
|
||||
placeholder: i18n.SEARCH_PLACEHOLDER,
|
||||
|
@ -47,12 +52,13 @@ const searchBox = {
|
|||
*/
|
||||
export const SearchRow = React.memo<Props>(
|
||||
({
|
||||
favoriteCount,
|
||||
onlyFavorites,
|
||||
onQueryChange,
|
||||
onToggleOnlyFavorites,
|
||||
query,
|
||||
totalSearchResultsCount,
|
||||
tabs,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<SearchRowContainer>
|
||||
|
@ -68,10 +74,11 @@ export const SearchRow = React.memo<Props>(
|
|||
data-test-subj="only-favorites-toggle"
|
||||
hasActiveFilters={onlyFavorites}
|
||||
onClick={onToggleOnlyFavorites}
|
||||
numFilters={favoriteCount ?? undefined}
|
||||
>
|
||||
{i18n.ONLY_FAVORITES}
|
||||
</EuiFilterButton>
|
||||
{tabs}
|
||||
{!!children && children}
|
||||
</>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
TimelineActionsOverflowColumns,
|
||||
} from '../types';
|
||||
import * as i18n from '../translations';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
|
||||
/**
|
||||
* Returns the action columns (e.g. delete, open duplicate timeline)
|
||||
|
@ -54,7 +55,9 @@ export const getActionsColumns = ({
|
|||
onClick: (selectedTimeline: OpenTimelineResult) => {
|
||||
if (enableExportTimelineDownloader != null) enableExportTimelineDownloader(selectedTimeline);
|
||||
},
|
||||
enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
|
||||
enabled: (timeline: OpenTimelineResult) => {
|
||||
return timeline.savedObjectId != null && timeline.status !== TimelineStatus.immutable;
|
||||
},
|
||||
description: i18n.EXPORT_SELECTED,
|
||||
'data-test-subj': 'export-timeline',
|
||||
};
|
||||
|
@ -65,7 +68,8 @@ export const getActionsColumns = ({
|
|||
onClick: (selectedTimeline: OpenTimelineResult) => {
|
||||
if (onOpenDeleteTimelineModal != null) onOpenDeleteTimelineModal(selectedTimeline);
|
||||
},
|
||||
enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
|
||||
enabled: ({ savedObjectId, status }: OpenTimelineResult) =>
|
||||
savedObjectId != null && status !== TimelineStatus.immutable,
|
||||
description: i18n.DELETE_SELECTED,
|
||||
'data-test-subj': 'delete-timeline',
|
||||
};
|
||||
|
|
|
@ -13,26 +13,18 @@ import { ACTION_COLUMN_WIDTH } from './common_styles';
|
|||
import { getNotesCount, getPinnedEventCount } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { FavoriteTimelineResult, OpenTimelineResult } from '../types';
|
||||
import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
/**
|
||||
* Returns the columns that have icon headers
|
||||
*/
|
||||
export const getIconHeaderColumns = () => [
|
||||
{
|
||||
align: 'center',
|
||||
field: 'pinnedEventIds',
|
||||
name: (
|
||||
<EuiToolTip content={i18n.PINNED_EVENTS}>
|
||||
<EuiIcon data-test-subj="pinned-event-header-icon" size="m" type="pin" />
|
||||
</EuiToolTip>
|
||||
),
|
||||
render: (_: Record<string, boolean> | null | undefined, timelineResult: OpenTimelineResult) => (
|
||||
<span data-test-subj="pinned-event-count">{`${getPinnedEventCount(timelineResult)}`}</span>
|
||||
),
|
||||
sortable: false,
|
||||
width: ACTION_COLUMN_WIDTH,
|
||||
},
|
||||
{
|
||||
export const getIconHeaderColumns = ({
|
||||
timelineType,
|
||||
}: {
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
}) => {
|
||||
const columns = {
|
||||
note: {
|
||||
align: 'center',
|
||||
field: 'eventIdToNoteIds',
|
||||
name: (
|
||||
|
@ -47,7 +39,24 @@ export const getIconHeaderColumns = () => [
|
|||
sortable: false,
|
||||
width: ACTION_COLUMN_WIDTH,
|
||||
},
|
||||
{
|
||||
pinnedEvent: {
|
||||
align: 'center',
|
||||
field: 'pinnedEventIds',
|
||||
name: (
|
||||
<EuiToolTip content={i18n.PINNED_EVENTS}>
|
||||
<EuiIcon data-test-subj="pinned-event-header-icon" size="m" type="pin" />
|
||||
</EuiToolTip>
|
||||
),
|
||||
render: (
|
||||
_: Record<string, boolean> | null | undefined,
|
||||
timelineResult: OpenTimelineResult
|
||||
) => (
|
||||
<span data-test-subj="pinned-event-count">{`${getPinnedEventCount(timelineResult)}`}</span>
|
||||
),
|
||||
sortable: false,
|
||||
width: ACTION_COLUMN_WIDTH,
|
||||
},
|
||||
favorite: {
|
||||
align: 'center',
|
||||
field: 'favorite',
|
||||
name: (
|
||||
|
@ -64,4 +73,8 @@ export const getIconHeaderColumns = () => [
|
|||
sortable: false,
|
||||
width: ACTION_COLUMN_WIDTH,
|
||||
},
|
||||
];
|
||||
};
|
||||
const templateColumns = [columns.note, columns.favorite];
|
||||
const defaultColumns = [columns.pinnedEvent, columns.note, columns.favorite];
|
||||
return timelineType === TimelineType.template ? templateColumns : defaultColumns;
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ import { getActionsColumns } from './actions_columns';
|
|||
import { getCommonColumns } from './common_columns';
|
||||
import { getExtendedColumns } from './extended_columns';
|
||||
import { getIconHeaderColumns } from './icon_header_columns';
|
||||
import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline';
|
||||
|
||||
// there are a number of type mismatches across this file
|
||||
const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
@ -58,6 +59,7 @@ export const getTimelinesTableColumns = ({
|
|||
onOpenTimeline,
|
||||
onToggleShowNotes,
|
||||
showExtendedColumns,
|
||||
timelineType,
|
||||
}: {
|
||||
actionTimelineToShow: ActionTimelineToShow[];
|
||||
deleteTimelines?: DeleteTimelines;
|
||||
|
@ -68,6 +70,7 @@ export const getTimelinesTableColumns = ({
|
|||
onSelectionChange: OnSelectionChange;
|
||||
onToggleShowNotes: OnToggleShowNotes;
|
||||
showExtendedColumns: boolean;
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
}) => {
|
||||
return [
|
||||
...getCommonColumns({
|
||||
|
@ -76,7 +79,7 @@ export const getTimelinesTableColumns = ({
|
|||
onToggleShowNotes,
|
||||
}),
|
||||
...getExtendedColumnsIfEnabled(showExtendedColumns),
|
||||
...getIconHeaderColumns(),
|
||||
...getIconHeaderColumns({ timelineType }),
|
||||
...getActionsColumns({
|
||||
actionTimelineToShow,
|
||||
deleteTimelines,
|
||||
|
@ -105,6 +108,7 @@ export interface TimelinesTableProps {
|
|||
showExtendedColumns: boolean;
|
||||
sortDirection: 'asc' | 'desc';
|
||||
sortField: string;
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tableRef?: React.MutableRefObject<_EuiBasicTable<any> | undefined>;
|
||||
totalSearchResultsCount: number;
|
||||
|
@ -134,6 +138,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
|
|||
sortField,
|
||||
sortDirection,
|
||||
tableRef,
|
||||
timelineType,
|
||||
totalSearchResultsCount,
|
||||
}) => {
|
||||
const pagination = {
|
||||
|
@ -174,6 +179,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
|
|||
onSelectionChange,
|
||||
onToggleShowNotes,
|
||||
showExtendedColumns,
|
||||
timelineType,
|
||||
})}
|
||||
compressed
|
||||
data-test-subj="timelines-table"
|
||||
|
|
|
@ -7,6 +7,7 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines_page';
|
|||
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
|
||||
import { OpenTimelineResult } from '../types';
|
||||
import { TimelinesTableProps } from '.';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
export const getMockTimelinesTableProps = (
|
||||
mockOpenTimelineResults: OpenTimelineResult[]
|
||||
|
@ -28,5 +29,6 @@ export const getMockTimelinesTableProps = (
|
|||
showExtendedColumns: true,
|
||||
sortDirection: DEFAULT_SORT_DIRECTION,
|
||||
sortField: DEFAULT_SORT_FIELD,
|
||||
timelineType: TimelineType.default,
|
||||
totalSearchResultsCount: mockOpenTimelineResults.length,
|
||||
});
|
||||
|
|
|
@ -220,6 +220,20 @@ export const TAB_TEMPLATES = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const FILTER_ELASTIC_TIMELINES = i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.templateFilter.elasticTitle',
|
||||
{
|
||||
defaultMessage: 'Elastic templates',
|
||||
}
|
||||
);
|
||||
|
||||
export const FILTER_CUSTOM_TIMELINES = i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.templateFilter.customizedTitle',
|
||||
{
|
||||
defaultMessage: 'Custom templates',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_TIMELINE_BTN_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.importTimelineModal.importTimelineTitle',
|
||||
{
|
||||
|
@ -230,7 +244,7 @@ export const IMPORT_TIMELINE_BTN_TITLE = i18n.translate(
|
|||
export const SELECT_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.importTimelineModal.selectTimelineDescription',
|
||||
{
|
||||
defaultMessage: 'Select a SIEM timeline (as exported from the Timeline view) to import',
|
||||
defaultMessage: 'Select a Security timeline (as exported from the Timeline view) to import',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -280,3 +294,10 @@ export const IMPORT_FAILED_DETAILED = (id: string, statusCode: number, message:
|
|||
defaultMessage: 'Timeline ID: {id}\n Status Code: {statusCode}\n Message: {message}',
|
||||
}
|
||||
);
|
||||
|
||||
export const TEMPLATE_CALL_OUT_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.timelines.components.templateCallOutMessageTitle',
|
||||
{
|
||||
defaultMessage: 'Now you can add timeline templates and link it to rules.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,7 +8,12 @@ import { SetStateAction, Dispatch } from 'react';
|
|||
import { AllTimelinesVariables } from '../../containers/all';
|
||||
import { TimelineModel } from '../../store/timeline/model';
|
||||
import { NoteResult } from '../../../graphql/types';
|
||||
import { TimelineTypeLiteral } from '../../../../common/types/timeline';
|
||||
import {
|
||||
TimelineTypeLiteral,
|
||||
TimelineTypeLiteralWithNull,
|
||||
TimelineStatus,
|
||||
TemplateTimelineTypeLiteral,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
/** The users who added a timeline to favorites */
|
||||
export interface FavoriteTimelineResult {
|
||||
|
@ -46,6 +51,7 @@ export interface OpenTimelineResult {
|
|||
notes?: TimelineResultNote[] | null;
|
||||
pinnedEventIds?: Readonly<Record<string, boolean>> | null;
|
||||
savedObjectId?: string | null;
|
||||
status?: TimelineStatus | null;
|
||||
title?: string | null;
|
||||
templateTimelineId?: string | null;
|
||||
type?: TimelineTypeLiteral;
|
||||
|
@ -118,6 +124,8 @@ export interface OpenTimelineProps {
|
|||
deleteTimelines?: DeleteTimelines;
|
||||
/** The default requested size of each page of search results */
|
||||
defaultPageSize: number;
|
||||
/** The number of favorite timeline*/
|
||||
favoriteCount?: number | null | undefined;
|
||||
/** Displays an indicator that data is loading when true */
|
||||
isLoading: boolean;
|
||||
/** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
|
||||
|
@ -160,8 +168,12 @@ export interface OpenTimelineProps {
|
|||
sortDirection: 'asc' | 'desc';
|
||||
/** the requested field to sort on */
|
||||
sortField: string;
|
||||
/** this affects timeline's behaviour like editable / duplicatible */
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
/** when timelineType === template, templatetimelineFilter is a JSX.Element */
|
||||
templateTimelineFilter: JSX.Element[] | null;
|
||||
/** timeline / template timeline */
|
||||
tabs?: JSX.Element;
|
||||
timelineFilter?: JSX.Element | JSX.Element[] | null;
|
||||
/** The title of the Open Timeline component */
|
||||
title: string;
|
||||
/** The total (server-side) count of the search results */
|
||||
|
@ -196,9 +208,19 @@ export enum TimelineTabsStyle {
|
|||
}
|
||||
|
||||
export interface TimelineTab {
|
||||
id: TimelineTypeLiteral;
|
||||
name: string;
|
||||
count: number | undefined;
|
||||
disabled: boolean;
|
||||
href: string;
|
||||
id: TimelineTypeLiteral;
|
||||
name: string;
|
||||
onClick: (ev: { preventDefault: () => void }) => void;
|
||||
withNext: boolean;
|
||||
}
|
||||
|
||||
export interface TemplateTimelineFilter {
|
||||
id: TemplateTimelineTypeLiteral;
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
withNext: boolean;
|
||||
count: number | undefined;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { EuiFilterButton } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
TimelineStatus,
|
||||
TimelineType,
|
||||
TimelineTypeLiteralWithNull,
|
||||
TemplateTimelineType,
|
||||
TemplateTimelineTypeLiteralWithNull,
|
||||
TimelineStatusLiteralWithNull,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { TemplateTimelineFilter } from './types';
|
||||
import { disableTemplate } from '../../../../common/constants';
|
||||
|
||||
export const useTimelineStatus = ({
|
||||
timelineType,
|
||||
elasticTemplateTimelineCount,
|
||||
customTemplateTimelineCount,
|
||||
}: {
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
elasticTemplateTimelineCount?: number | null;
|
||||
customTemplateTimelineCount?: number | null;
|
||||
}): {
|
||||
timelineStatus: TimelineStatusLiteralWithNull;
|
||||
templateTimelineType: TemplateTimelineTypeLiteralWithNull;
|
||||
templateTimelineFilter: JSX.Element[] | null;
|
||||
} => {
|
||||
const [selectedTab, setSelectedTab] = useState<TemplateTimelineTypeLiteralWithNull>(
|
||||
disableTemplate ? null : TemplateTimelineType.elastic
|
||||
);
|
||||
const isTemplateFilterEnabled = useMemo(() => timelineType === TimelineType.template, [
|
||||
timelineType,
|
||||
]);
|
||||
|
||||
const templateTimelineType = useMemo(
|
||||
() => (disableTemplate || !isTemplateFilterEnabled ? null : selectedTab),
|
||||
[selectedTab, isTemplateFilterEnabled]
|
||||
);
|
||||
|
||||
const timelineStatus = useMemo(
|
||||
() =>
|
||||
templateTimelineType == null
|
||||
? null
|
||||
: templateTimelineType === TemplateTimelineType.elastic
|
||||
? TimelineStatus.immutable
|
||||
: TimelineStatus.active,
|
||||
[templateTimelineType]
|
||||
);
|
||||
|
||||
const filters = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: TemplateTimelineType.elastic,
|
||||
name: i18n.FILTER_ELASTIC_TIMELINES,
|
||||
disabled: !isTemplateFilterEnabled,
|
||||
withNext: true,
|
||||
count: elasticTemplateTimelineCount ?? undefined,
|
||||
},
|
||||
{
|
||||
id: TemplateTimelineType.custom,
|
||||
name: i18n.FILTER_CUSTOM_TIMELINES,
|
||||
disabled: !isTemplateFilterEnabled,
|
||||
withNext: false,
|
||||
count: customTemplateTimelineCount ?? undefined,
|
||||
},
|
||||
],
|
||||
[customTemplateTimelineCount, elasticTemplateTimelineCount, isTemplateFilterEnabled]
|
||||
);
|
||||
|
||||
const onFilterClicked = useCallback(
|
||||
(tabId) => {
|
||||
if (selectedTab === tabId) {
|
||||
setSelectedTab(null);
|
||||
} else {
|
||||
setSelectedTab(tabId);
|
||||
}
|
||||
},
|
||||
[setSelectedTab, selectedTab]
|
||||
);
|
||||
|
||||
const templateTimelineFilter = useMemo(() => {
|
||||
return isTemplateFilterEnabled
|
||||
? filters.map((tab: TemplateTimelineFilter) => (
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={tab.id === templateTimelineType}
|
||||
key={`template-timeline-filter-${tab.id}`}
|
||||
numFilters={tab.count}
|
||||
onClick={onFilterClicked.bind(null, tab.id)}
|
||||
withNext={tab.withNext}
|
||||
isDisabled={tab.disabled}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiFilterButton>
|
||||
))
|
||||
: null;
|
||||
}, [templateTimelineType, filters, isTemplateFilterEnabled, onFilterClicked]);
|
||||
|
||||
return {
|
||||
timelineStatus,
|
||||
templateTimelineType,
|
||||
templateTimelineFilter,
|
||||
};
|
||||
};
|
|
@ -13,10 +13,16 @@ import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/lin
|
|||
import * as i18n from './translations';
|
||||
import { TimelineTabsStyle, TimelineTab } from './types';
|
||||
|
||||
export const useTimelineTypes = (): {
|
||||
export const useTimelineTypes = ({
|
||||
defaultTimelineCount,
|
||||
templateTimelineCount,
|
||||
}: {
|
||||
defaultTimelineCount?: number | null;
|
||||
templateTimelineCount?: number | null;
|
||||
}): {
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
timelineTabs: JSX.Element;
|
||||
timelineFilters: JSX.Element;
|
||||
timelineFilters: JSX.Element[];
|
||||
} => {
|
||||
const history = useHistory();
|
||||
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines);
|
||||
|
@ -40,10 +46,8 @@ export const useTimelineTypes = (): {
|
|||
},
|
||||
[history, urlSearch]
|
||||
);
|
||||
|
||||
const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = (
|
||||
timelineTabsStyle: TimelineTabsStyle
|
||||
) => [
|
||||
const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = useCallback(
|
||||
(timelineTabsStyle: TimelineTabsStyle) => [
|
||||
{
|
||||
id: TimelineType.default,
|
||||
name:
|
||||
|
@ -52,6 +56,11 @@ export const useTimelineTypes = (): {
|
|||
: i18n.TAB_TIMELINES,
|
||||
href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)),
|
||||
disabled: false,
|
||||
withNext: true,
|
||||
count:
|
||||
timelineTabsStyle === TimelineTabsStyle.filter
|
||||
? defaultTimelineCount ?? undefined
|
||||
: undefined,
|
||||
onClick: goToTimeline,
|
||||
},
|
||||
{
|
||||
|
@ -62,13 +71,27 @@ export const useTimelineTypes = (): {
|
|||
: i18n.TAB_TEMPLATES,
|
||||
href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)),
|
||||
disabled: false,
|
||||
withNext: false,
|
||||
count:
|
||||
timelineTabsStyle === TimelineTabsStyle.filter
|
||||
? templateTimelineCount ?? undefined
|
||||
: undefined,
|
||||
onClick: goToTemplateTimeline,
|
||||
},
|
||||
];
|
||||
],
|
||||
[
|
||||
defaultTimelineCount,
|
||||
templateTimelineCount,
|
||||
urlSearch,
|
||||
formatUrl,
|
||||
goToTimeline,
|
||||
goToTemplateTimeline,
|
||||
]
|
||||
);
|
||||
|
||||
const onFilterClicked = useCallback(
|
||||
(timelineTabsStyle, tabId) => {
|
||||
if (timelineTabsStyle === TimelineTabsStyle.filter && tabId === timelineType) {
|
||||
(tabId) => {
|
||||
if (tabId === timelineType) {
|
||||
setTimelineTypes(null);
|
||||
} else {
|
||||
setTimelineTypes(tabId);
|
||||
|
@ -89,7 +112,7 @@ export const useTimelineTypes = (): {
|
|||
href={tab.href}
|
||||
onClick={(ev) => {
|
||||
tab.onClick(ev);
|
||||
onFilterClicked(TimelineTabsStyle.tab, tab.id);
|
||||
onFilterClicked(tab.id);
|
||||
}}
|
||||
>
|
||||
{tab.name}
|
||||
|
@ -103,24 +126,21 @@ export const useTimelineTypes = (): {
|
|||
}, [tabName]);
|
||||
|
||||
const timelineFilters = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => (
|
||||
return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => (
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={tab.id === timelineType}
|
||||
key={`timeline-${TimelineTabsStyle.filter}-${tab.id}`}
|
||||
numFilters={tab.count}
|
||||
onClick={(ev: { preventDefault: () => void }) => {
|
||||
tab.onClick(ev);
|
||||
onFilterClicked.bind(null, TimelineTabsStyle.filter, tab.id);
|
||||
onFilterClicked(tab.id);
|
||||
}}
|
||||
withNext={tab.withNext}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiFilterButton>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [timelineType]);
|
||||
));
|
||||
}, [timelineType, getFilterOrTabs, onFilterClicked]);
|
||||
|
||||
return {
|
||||
timelineType,
|
||||
|
|
|
@ -926,6 +926,7 @@ In other use cases the message field can be used to concatenate different values
|
|||
}
|
||||
}
|
||||
start={1521830963132}
|
||||
status="active"
|
||||
toggleColumn={[MockFunction]}
|
||||
usersViewing={
|
||||
Array [
|
||||
|
|
|
@ -5,13 +5,24 @@
|
|||
*/
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { TestProviders, mockTimelineModel } from '../../../../../common/mock';
|
||||
import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
|
||||
|
||||
import { Actions } from '.';
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const origin = jest.requireActual('react-redux');
|
||||
return {
|
||||
...origin,
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
(useSelector as jest.Mock).mockReturnValue(mockTimelineModel);
|
||||
|
||||
test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -3,10 +3,15 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { Note } from '../../../../../common/lib/note';
|
||||
import { StoreState } from '../../../../../common/store/types';
|
||||
|
||||
import { TimelineModel } from '../../../../store/timeline/model';
|
||||
|
||||
import { AssociateNote, UpdateNote } from '../../../notes/helpers';
|
||||
import { Pin } from '../../pin';
|
||||
import { NotesButton } from '../../properties/helpers';
|
||||
|
@ -79,7 +84,11 @@ export const Actions = React.memo<Props>(
|
|||
showNotes,
|
||||
toggleShowNotes,
|
||||
updateNote,
|
||||
}) => (
|
||||
}) => {
|
||||
const timeline = useSelector<StoreState, TimelineModel>((state) => {
|
||||
return state.timeline.timelineById['timeline-1'];
|
||||
});
|
||||
return (
|
||||
<EventsTdGroupActions
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
data-test-subj="event-actions-container"
|
||||
|
@ -133,6 +142,7 @@ export const Actions = React.memo<Props>(
|
|||
content={getPinTooltip({
|
||||
isPinned: eventIsPinned,
|
||||
eventHasNotes: eventHasNotes(noteIds),
|
||||
timelineType: timeline.timelineType,
|
||||
})}
|
||||
>
|
||||
<Pin
|
||||
|
@ -140,6 +150,7 @@ export const Actions = React.memo<Props>(
|
|||
data-test-subj="pin-event"
|
||||
onClick={onPinClicked}
|
||||
pinned={eventIsPinned}
|
||||
timelineType={timeline.timelineType}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EventsTdContent>
|
||||
|
@ -155,8 +166,10 @@ export const Actions = React.memo<Props>(
|
|||
noteIds={noteIds || emptyNotes}
|
||||
showNotes={showNotes}
|
||||
size="s"
|
||||
status={timeline.status}
|
||||
timelineType={timeline.timelineType}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
toolTip={i18n.NOTES_TOOLTIP}
|
||||
toolTip={timeline.timelineType ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
</EventsTdContent>
|
||||
|
@ -164,7 +177,8 @@ export const Actions = React.memo<Props>(
|
|||
</>
|
||||
)}
|
||||
</EventsTdGroupActions>
|
||||
),
|
||||
);
|
||||
},
|
||||
(nextProps, prevProps) => {
|
||||
return (
|
||||
prevProps.actionsColumnWidth === nextProps.actionsColumnWidth &&
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import uuid from 'uuid';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
|
||||
|
@ -13,7 +14,7 @@ import { TimelineDetailsQuery } from '../../../../containers/details';
|
|||
import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types';
|
||||
import { requestIdleCallbackViaScheduler } from '../../../../../common/lib/helpers/scheduler';
|
||||
import { Note } from '../../../../../common/lib/note';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model';
|
||||
import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers';
|
||||
import { SkeletonRow } from '../../skeleton_row';
|
||||
import {
|
||||
|
@ -33,6 +34,7 @@ import { getEventType } from '../helpers';
|
|||
import { NoteCards } from '../../../notes/note_cards';
|
||||
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import { StoreState } from '../../../../../common/store';
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
|
@ -128,7 +130,9 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({});
|
||||
const [initialRender, setInitialRender] = useState(false);
|
||||
const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
|
||||
|
||||
const timeline = useSelector<StoreState, TimelineModel>((state) => {
|
||||
return state.timeline.timelineById['timeline-1'];
|
||||
});
|
||||
const divElement = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const onToggleShowNotes = useCallback(() => {
|
||||
|
@ -251,6 +255,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
getNotesByIds={getNotesByIds}
|
||||
noteIds={eventIdToNoteIds[event._id] || emptyNotes}
|
||||
showAddNote={!!showNotes[event._id]}
|
||||
status={timeline.status}
|
||||
toggleShowAddNote={onToggleShowNotes}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { Ecs } from '../../../../graphql/types';
|
||||
|
||||
import { eventHasNotes, eventIsPinned, getPinTooltip, stringifyEvent } from './helpers';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('stringifyEvent', () => {
|
||||
|
@ -192,21 +193,37 @@ describe('helpers', () => {
|
|||
|
||||
describe('getPinTooltip', () => {
|
||||
test('it indicates the event may NOT be unpinned when `isPinned` is `true` and the event has notes', () => {
|
||||
expect(getPinTooltip({ isPinned: true, eventHasNotes: true })).toEqual(
|
||||
'This event cannot be unpinned because it has notes'
|
||||
);
|
||||
expect(
|
||||
getPinTooltip({ isPinned: true, eventHasNotes: true, timelineType: TimelineType.default })
|
||||
).toEqual('This event cannot be unpinned because it has notes');
|
||||
});
|
||||
|
||||
test('it indicates the event is pinned when `isPinned` is `true` and the event does NOT have notes', () => {
|
||||
expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual('Pinned event');
|
||||
expect(
|
||||
getPinTooltip({ isPinned: true, eventHasNotes: false, timelineType: TimelineType.default })
|
||||
).toEqual('Pinned event');
|
||||
});
|
||||
|
||||
test('it indicates the event is NOT pinned when `isPinned` is `false` and the event has notes', () => {
|
||||
expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual('Unpinned event');
|
||||
expect(
|
||||
getPinTooltip({ isPinned: false, eventHasNotes: true, timelineType: TimelineType.default })
|
||||
).toEqual('Unpinned event');
|
||||
});
|
||||
|
||||
test('it indicates the event is NOT pinned when `isPinned` is `false` and the event does NOT have notes', () => {
|
||||
expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual('Unpinned event');
|
||||
expect(
|
||||
getPinTooltip({ isPinned: false, eventHasNotes: false, timelineType: TimelineType.default })
|
||||
).toEqual('Unpinned event');
|
||||
});
|
||||
|
||||
test('it indicates the event is disabled if timelineType is template', () => {
|
||||
expect(
|
||||
getPinTooltip({
|
||||
isPinned: false,
|
||||
eventHasNotes: false,
|
||||
timelineType: TimelineType.template,
|
||||
})
|
||||
).toEqual('This event cannot be pinned because it is filtered by a timeline template');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import { OnPinEvent, OnUnPinEvent } from '../events';
|
|||
import { TimelineRowAction, TimelineRowActionOnClick } from './actions';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const omitTypenameAndEmpty = (k: string, v: any): any | undefined =>
|
||||
|
@ -28,10 +29,19 @@ export const getPinTooltip = ({
|
|||
isPinned,
|
||||
// eslint-disable-next-line no-shadow
|
||||
eventHasNotes,
|
||||
timelineType,
|
||||
}: {
|
||||
isPinned: boolean;
|
||||
eventHasNotes: boolean;
|
||||
}) => (isPinned && eventHasNotes ? i18n.PINNED_WITH_NOTES : isPinned ? i18n.PINNED : i18n.UNPINNED);
|
||||
timelineType: TimelineTypeLiteral;
|
||||
}) =>
|
||||
timelineType === TimelineType.template
|
||||
? i18n.DISABLE_PIN
|
||||
: isPinned && eventHasNotes
|
||||
? i18n.PINNED_WITH_NOTES
|
||||
: isPinned
|
||||
? i18n.PINNED
|
||||
: i18n.UNPINNED;
|
||||
|
||||
export interface IsPinnedParams {
|
||||
eventId: string;
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { mockBrowserFields } from '../../../../common/containers/source/mock';
|
||||
import { Direction } from '../../../../graphql/types';
|
||||
import { defaultHeaders, mockTimelineData } from '../../../../common/mock';
|
||||
import { defaultHeaders, mockTimelineData, mockTimelineModel } from '../../../../common/mock';
|
||||
import { TestProviders } from '../../../../common/mock/test_providers';
|
||||
|
||||
import { Body, BodyProps } from '.';
|
||||
|
@ -24,6 +25,13 @@ const mockSort: Sort = {
|
|||
sortDirection: Direction.desc,
|
||||
};
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const origin = jest.requireActual('react-redux');
|
||||
return {
|
||||
...origin,
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/components/link_to');
|
||||
|
||||
jest.mock(
|
||||
|
@ -41,41 +49,43 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({
|
|||
|
||||
describe('Body', () => {
|
||||
const mount = useMountAppended();
|
||||
const props: BodyProps = {
|
||||
addNoteToEvent: jest.fn(),
|
||||
browserFields: mockBrowserFields,
|
||||
columnHeaders: defaultHeaders,
|
||||
columnRenderers,
|
||||
data: mockTimelineData,
|
||||
eventIdToNoteIds: {},
|
||||
height: testBodyHeight,
|
||||
id: 'timeline-test',
|
||||
isSelectAllChecked: false,
|
||||
getNotesByIds: mockGetNotesByIds,
|
||||
loadingEventIds: [],
|
||||
onColumnRemoved: jest.fn(),
|
||||
onColumnResized: jest.fn(),
|
||||
onColumnSorted: jest.fn(),
|
||||
onFilterChange: jest.fn(),
|
||||
onPinEvent: jest.fn(),
|
||||
onRowSelected: jest.fn(),
|
||||
onSelectAll: jest.fn(),
|
||||
onUnPinEvent: jest.fn(),
|
||||
onUpdateColumns: jest.fn(),
|
||||
pinnedEventIds: {},
|
||||
rowRenderers,
|
||||
selectedEventIds: {},
|
||||
show: true,
|
||||
sort: mockSort,
|
||||
showCheckboxes: false,
|
||||
toggleColumn: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
};
|
||||
(useSelector as jest.Mock).mockReturnValue(mockTimelineModel);
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the column headers', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addNoteToEvent={jest.fn()}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockTimelineData}
|
||||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
show={true}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<Body {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -85,36 +95,7 @@ describe('Body', () => {
|
|||
test('it renders the scroll container', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addNoteToEvent={jest.fn()}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockTimelineData}
|
||||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
show={true}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<Body {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -124,36 +105,7 @@ describe('Body', () => {
|
|||
test('it renders events', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addNoteToEvent={jest.fn()}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockTimelineData}
|
||||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
show={true}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<Body {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -162,39 +114,10 @@ describe('Body', () => {
|
|||
|
||||
test('it renders a tooltip for timestamp', async () => {
|
||||
const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp');
|
||||
|
||||
const testProps = { ...props, columnHeaders: headersJustTimestamp };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addNoteToEvent={jest.fn()}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={headersJustTimestamp}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockTimelineData}
|
||||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
show={true}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<Body {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
@ -215,6 +138,11 @@ describe('Body', () => {
|
|||
describe('action on event', () => {
|
||||
const dispatchAddNoteToEvent = jest.fn();
|
||||
const dispatchOnPinEvent = jest.fn();
|
||||
const testProps = {
|
||||
...props,
|
||||
addNoteToEvent: dispatchAddNoteToEvent,
|
||||
onPinEvent: dispatchOnPinEvent,
|
||||
};
|
||||
|
||||
const addaNoteToEvent = (wrapper: ReturnType<typeof mount>, note: string) => {
|
||||
wrapper.find('[data-test-subj="add-note"]').first().find('button').simulate('click');
|
||||
|
@ -251,36 +179,7 @@ describe('Body', () => {
|
|||
test('Add a Note to an event', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Body
|
||||
addNoteToEvent={dispatchAddNoteToEvent}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockTimelineData}
|
||||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={dispatchOnPinEvent}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
show={true}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
<Body {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
addaNoteToEvent(wrapper, 'hello world');
|
||||
|
@ -290,44 +189,13 @@ describe('Body', () => {
|
|||
});
|
||||
|
||||
test('Add two Note to an event', () => {
|
||||
const Proxy = (props: BodyProps) => (
|
||||
const Proxy = (proxyProps: BodyProps) => (
|
||||
<TestProviders>
|
||||
<Body {...props} />
|
||||
<Body {...proxyProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const wrapper = mount(
|
||||
<Proxy
|
||||
addNoteToEvent={dispatchAddNoteToEvent}
|
||||
browserFields={mockBrowserFields}
|
||||
columnHeaders={defaultHeaders}
|
||||
columnRenderers={columnRenderers}
|
||||
data={mockTimelineData}
|
||||
eventIdToNoteIds={{}}
|
||||
height={testBodyHeight}
|
||||
id={'timeline-test'}
|
||||
isSelectAllChecked={false}
|
||||
getNotesByIds={mockGetNotesByIds}
|
||||
loadingEventIds={[]}
|
||||
onColumnRemoved={jest.fn()}
|
||||
onColumnResized={jest.fn()}
|
||||
onColumnSorted={jest.fn()}
|
||||
onFilterChange={jest.fn()}
|
||||
onPinEvent={dispatchOnPinEvent}
|
||||
onRowSelected={jest.fn()}
|
||||
onSelectAll={jest.fn()}
|
||||
onUnPinEvent={jest.fn()}
|
||||
onUpdateColumns={jest.fn()}
|
||||
pinnedEventIds={{}}
|
||||
rowRenderers={rowRenderers}
|
||||
selectedEventIds={{}}
|
||||
show={true}
|
||||
sort={mockSort}
|
||||
showCheckboxes={false}
|
||||
toggleColumn={jest.fn()}
|
||||
updateNote={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const wrapper = mount(<Proxy {...testProps} />);
|
||||
addaNoteToEvent(wrapper, 'hello world');
|
||||
dispatchAddNoteToEvent.mockClear();
|
||||
dispatchOnPinEvent.mockClear();
|
||||
|
|
|
@ -13,6 +13,13 @@ export const NOTES_TOOLTIP = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const NOTES_DISABLE_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.notes.disableEventTooltip',
|
||||
{
|
||||
defaultMessage: 'Add notes for event filtered by a timeline template is not allowed',
|
||||
}
|
||||
);
|
||||
|
||||
export const COPY_TO_CLIPBOARD = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.copyToClipboardButtonLabel',
|
||||
{
|
||||
|
@ -38,6 +45,13 @@ export const PINNED_WITH_NOTES = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const DISABLE_PIN = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.pinning.disablePinnnedTooltip',
|
||||
{
|
||||
defaultMessage: 'This event cannot be pinned because it is filtered by a timeline template',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPAND = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.actions.expandAriaLabel',
|
||||
{
|
||||
|
|
|
@ -15,6 +15,7 @@ import { mockDataProviders } from '../data_providers/mock/mock_data_providers';
|
|||
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
|
||||
|
||||
import { TimelineHeader } from '.';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
|
||||
const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings;
|
||||
|
||||
|
@ -23,43 +24,32 @@ jest.mock('../../../../common/lib/kibana');
|
|||
describe('Header', () => {
|
||||
const indexPattern = mockIndexPattern;
|
||||
const mount = useMountAppended();
|
||||
const props = {
|
||||
browserFields: {},
|
||||
dataProviders: mockDataProviders,
|
||||
filterManager: new FilterManager(mockUiSettingsForFilterManager),
|
||||
id: 'foo',
|
||||
indexPattern,
|
||||
onDataProviderEdited: jest.fn(),
|
||||
onDataProviderRemoved: jest.fn(),
|
||||
onToggleDataProviderEnabled: jest.fn(),
|
||||
onToggleDataProviderExcluded: jest.fn(),
|
||||
show: true,
|
||||
showCallOutUnauthorizedMsg: false,
|
||||
status: TimelineStatus.active,
|
||||
};
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TimelineHeader
|
||||
browserFields={{}}
|
||||
dataProviders={mockDataProviders}
|
||||
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
|
||||
id="foo"
|
||||
indexPattern={indexPattern}
|
||||
onDataProviderEdited={jest.fn()}
|
||||
onDataProviderRemoved={jest.fn()}
|
||||
onToggleDataProviderEnabled={jest.fn()}
|
||||
onToggleDataProviderExcluded={jest.fn()}
|
||||
show={true}
|
||||
showCallOutUnauthorizedMsg={false}
|
||||
/>
|
||||
);
|
||||
const wrapper = shallow(<TimelineHeader {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders the data providers when show is true', () => {
|
||||
const testProps = { ...props, show: true };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<TimelineHeader
|
||||
browserFields={{}}
|
||||
dataProviders={mockDataProviders}
|
||||
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
|
||||
id="foo"
|
||||
indexPattern={indexPattern}
|
||||
onDataProviderEdited={jest.fn()}
|
||||
onDataProviderRemoved={jest.fn()}
|
||||
onToggleDataProviderEnabled={jest.fn()}
|
||||
onToggleDataProviderExcluded={jest.fn()}
|
||||
show={true}
|
||||
showCallOutUnauthorizedMsg={false}
|
||||
/>
|
||||
<TimelineHeader {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -67,21 +57,11 @@ describe('Header', () => {
|
|||
});
|
||||
|
||||
test('it does NOT render the data providers when show is false', () => {
|
||||
const testProps = { ...props, show: false };
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<TimelineHeader
|
||||
browserFields={{}}
|
||||
dataProviders={mockDataProviders}
|
||||
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
|
||||
id="foo"
|
||||
indexPattern={indexPattern}
|
||||
onDataProviderEdited={jest.fn()}
|
||||
onDataProviderRemoved={jest.fn()}
|
||||
onToggleDataProviderEnabled={jest.fn()}
|
||||
onToggleDataProviderExcluded={jest.fn()}
|
||||
show={false}
|
||||
showCallOutUnauthorizedMsg={false}
|
||||
/>
|
||||
<TimelineHeader {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -89,21 +69,15 @@ describe('Header', () => {
|
|||
});
|
||||
|
||||
test('it renders the unauthorized call out providers', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
filterManager: new FilterManager(mockUiSettingsForFilterManager),
|
||||
showCallOutUnauthorizedMsg: true,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<TimelineHeader
|
||||
browserFields={{}}
|
||||
dataProviders={mockDataProviders}
|
||||
filterManager={new FilterManager(mockUiSettingsForFilterManager)}
|
||||
id="foo"
|
||||
indexPattern={indexPattern}
|
||||
onDataProviderEdited={jest.fn()}
|
||||
onDataProviderRemoved={jest.fn()}
|
||||
onToggleDataProviderEnabled={jest.fn()}
|
||||
onToggleDataProviderExcluded={jest.fn()}
|
||||
show={true}
|
||||
showCallOutUnauthorizedMsg={true}
|
||||
/>
|
||||
<TimelineHeader {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -22,6 +22,10 @@ import { StatefulSearchOrFilter } from '../search_or_filter';
|
|||
import { BrowserFields } from '../../../../common/containers/source';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import {
|
||||
TimelineStatus,
|
||||
TimelineStatusLiteralWithNull,
|
||||
} from '../../../../../common/types/timeline';
|
||||
|
||||
interface Props {
|
||||
browserFields: BrowserFields;
|
||||
|
@ -36,6 +40,7 @@ interface Props {
|
|||
onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
|
||||
show: boolean;
|
||||
showCallOutUnauthorizedMsg: boolean;
|
||||
status: TimelineStatusLiteralWithNull;
|
||||
}
|
||||
|
||||
const TimelineHeaderComponent: React.FC<Props> = ({
|
||||
|
@ -51,6 +56,7 @@ const TimelineHeaderComponent: React.FC<Props> = ({
|
|||
onToggleDataProviderExcluded,
|
||||
show,
|
||||
showCallOutUnauthorizedMsg,
|
||||
status,
|
||||
}) => (
|
||||
<>
|
||||
{showCallOutUnauthorizedMsg && (
|
||||
|
@ -62,7 +68,15 @@ const TimelineHeaderComponent: React.FC<Props> = ({
|
|||
size="s"
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === TimelineStatus.immutable && (
|
||||
<EuiCallOut
|
||||
data-test-subj="timelineImmutableCallOut"
|
||||
title={i18n.CALL_OUT_IMMUTIABLE}
|
||||
color="primary"
|
||||
iconType="info"
|
||||
size="s"
|
||||
/>
|
||||
)}
|
||||
{show && !showGraphView(graphEventId) && (
|
||||
<>
|
||||
<DataProviders
|
||||
|
@ -100,5 +114,6 @@ export const TimelineHeader = React.memo(
|
|||
prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled &&
|
||||
prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded &&
|
||||
prevProps.show === nextProps.show &&
|
||||
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg
|
||||
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
|
||||
prevProps.status === nextProps.status
|
||||
);
|
||||
|
|
|
@ -13,3 +13,11 @@ export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate(
|
|||
'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CALL_OUT_IMMUTIABLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.callOut.immutable.message.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'This timeline is immutable, therefore not allowed to save it within the security application, though you may continue to use the timeline to search and filter security events',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -25,6 +25,7 @@ import { Sort } from './body/sort';
|
|||
import { mockDataProviders } from './data_providers/mock/mock_data_providers';
|
||||
import { StatefulTimeline, Props as StatefulTimelineProps } from './index';
|
||||
import { Timeline } from './timeline';
|
||||
import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../common/lib/kibana');
|
||||
|
@ -88,6 +89,8 @@ describe('StatefulTimeline', () => {
|
|||
showCallOutUnauthorizedMsg: false,
|
||||
sort,
|
||||
start: startDate,
|
||||
status: TimelineStatus.active,
|
||||
timelineType: TimelineType.default,
|
||||
updateColumns: timelineActions.updateColumns,
|
||||
updateDataProviderEnabled: timelineActions.updateDataProviderEnabled,
|
||||
updateDataProviderExcluded: timelineActions.updateDataProviderExcluded,
|
||||
|
|
|
@ -57,6 +57,8 @@ const StatefulTimelineComponent = React.memo<Props>(
|
|||
showCallOutUnauthorizedMsg,
|
||||
sort,
|
||||
start,
|
||||
status,
|
||||
timelineType,
|
||||
updateDataProviderEnabled,
|
||||
updateDataProviderExcluded,
|
||||
updateItemsPerPage,
|
||||
|
@ -189,6 +191,7 @@ const StatefulTimelineComponent = React.memo<Props>(
|
|||
showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg}
|
||||
sort={sort!}
|
||||
start={start}
|
||||
status={status}
|
||||
toggleColumn={toggleColumn}
|
||||
usersViewing={usersViewing}
|
||||
/>
|
||||
|
@ -207,6 +210,8 @@ const StatefulTimelineComponent = React.memo<Props>(
|
|||
prevProps.show === nextProps.show &&
|
||||
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
|
||||
prevProps.start === nextProps.start &&
|
||||
prevProps.timelineType === nextProps.timelineType &&
|
||||
prevProps.status === nextProps.status &&
|
||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
|
||||
deepEqual(prevProps.filters, nextProps.filters) &&
|
||||
|
@ -238,11 +243,12 @@ const makeMapStateToProps = () => {
|
|||
kqlMode,
|
||||
show,
|
||||
sort,
|
||||
status,
|
||||
timelineType,
|
||||
} = timeline;
|
||||
const kqlQueryExpression = getKqlQueryTimeline(state, id)!;
|
||||
|
||||
const timelineFilter = kqlMode === 'filter' ? filters || [] : [];
|
||||
|
||||
return {
|
||||
columns,
|
||||
dataProviders,
|
||||
|
@ -261,6 +267,8 @@ const makeMapStateToProps = () => {
|
|||
showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state),
|
||||
sort,
|
||||
start: input.timerange.from,
|
||||
status,
|
||||
timelineType,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -8,6 +8,8 @@ import { EuiButtonIcon, IconSize } from '@elastic/eui';
|
|||
import { noop } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { TimelineType, TimelineTypeLiteral } from '../../../../../common/types/timeline';
|
||||
|
||||
import * as i18n from '../body/translations';
|
||||
|
||||
export type PinIcon = 'pin' | 'pinFilled';
|
||||
|
@ -17,21 +19,25 @@ export const getPinIcon = (pinned: boolean): PinIcon => (pinned ? 'pinFilled' :
|
|||
interface Props {
|
||||
allowUnpinning: boolean;
|
||||
iconSize?: IconSize;
|
||||
timelineType?: TimelineTypeLiteral;
|
||||
onClick?: () => void;
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
export const Pin = React.memo<Props>(
|
||||
({ allowUnpinning, iconSize = 'm', onClick = noop, pinned }) => (
|
||||
({ allowUnpinning, iconSize = 'm', onClick = noop, pinned, timelineType }) => {
|
||||
const isTemplate = timelineType === TimelineType.template;
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={pinned ? i18n.PINNED : i18n.UNPINNED}
|
||||
data-test-subj="pin"
|
||||
iconSize={iconSize}
|
||||
iconType={getPinIcon(pinned)}
|
||||
isDisabled={allowUnpinning ? false : true}
|
||||
onClick={onClick}
|
||||
isDisabled={isTemplate}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Pin.displayName = 'Pin';
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
TimelineTypeLiteral,
|
||||
TimelineStatus,
|
||||
TimelineType,
|
||||
TimelineStatusLiteral,
|
||||
TimelineId,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
|
@ -262,11 +263,13 @@ interface NotesButtonProps {
|
|||
getNotesByIds: (noteIds: string[]) => Note[];
|
||||
noteIds: string[];
|
||||
size: 's' | 'l';
|
||||
status: TimelineStatusLiteral;
|
||||
showNotes: boolean;
|
||||
toggleShowNotes: () => void;
|
||||
text?: string;
|
||||
toolTip?: string;
|
||||
updateNote: UpdateNote;
|
||||
timelineType: TimelineTypeLiteral;
|
||||
}
|
||||
|
||||
const getNewNoteId = (): string => uuid.v4();
|
||||
|
@ -303,16 +306,24 @@ LargeNotesButton.displayName = 'LargeNotesButton';
|
|||
interface SmallNotesButtonProps {
|
||||
noteIds: string[];
|
||||
toggleShowNotes: () => void;
|
||||
timelineType: TimelineTypeLiteral;
|
||||
}
|
||||
|
||||
const SmallNotesButton = React.memo<SmallNotesButtonProps>(({ noteIds, toggleShowNotes }) => (
|
||||
const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||
({ noteIds, toggleShowNotes, timelineType }) => {
|
||||
const isTemplate = timelineType === TimelineType.template;
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.NOTES}
|
||||
data-test-subj="timeline-notes-button-small"
|
||||
iconType="editorComment"
|
||||
onClick={() => toggleShowNotes()}
|
||||
isDisabled={isTemplate}
|
||||
/>
|
||||
));
|
||||
);
|
||||
}
|
||||
);
|
||||
SmallNotesButton.displayName = 'SmallNotesButton';
|
||||
|
||||
/**
|
||||
|
@ -326,25 +337,32 @@ const NotesButtonComponent = React.memo<NotesButtonProps>(
|
|||
noteIds,
|
||||
showNotes,
|
||||
size,
|
||||
status,
|
||||
toggleShowNotes,
|
||||
text,
|
||||
updateNote,
|
||||
timelineType,
|
||||
}) => (
|
||||
<ButtonContainer animate={animate} data-test-subj="timeline-notes-button-container">
|
||||
<>
|
||||
{size === 'l' ? (
|
||||
<LargeNotesButton noteIds={noteIds} text={text} toggleShowNotes={toggleShowNotes} />
|
||||
) : (
|
||||
<SmallNotesButton noteIds={noteIds} toggleShowNotes={toggleShowNotes} />
|
||||
<SmallNotesButton
|
||||
noteIds={noteIds}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
timelineType={timelineType}
|
||||
/>
|
||||
)}
|
||||
{size === 'l' && showNotes ? (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal maxWidth={NOTES_PANEL_WIDTH} onClose={toggleShowNotes}>
|
||||
<Notes
|
||||
associateNote={associateNote}
|
||||
getNotesByIds={getNotesByIds}
|
||||
noteIds={noteIds}
|
||||
getNewNoteId={getNewNoteId}
|
||||
getNotesByIds={getNotesByIds}
|
||||
status={status}
|
||||
noteIds={noteIds}
|
||||
updateNote={updateNote}
|
||||
/>
|
||||
</EuiModal>
|
||||
|
@ -364,6 +382,8 @@ export const NotesButton = React.memo<NotesButtonProps>(
|
|||
noteIds,
|
||||
showNotes,
|
||||
size,
|
||||
status,
|
||||
timelineType,
|
||||
toggleShowNotes,
|
||||
toolTip,
|
||||
text,
|
||||
|
@ -377,9 +397,11 @@ export const NotesButton = React.memo<NotesButtonProps>(
|
|||
noteIds={noteIds}
|
||||
showNotes={showNotes}
|
||||
size={size}
|
||||
status={status}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
text={text}
|
||||
updateNote={updateNote}
|
||||
timelineType={timelineType}
|
||||
/>
|
||||
) : (
|
||||
<EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip">
|
||||
|
@ -390,9 +412,11 @@ export const NotesButton = React.memo<NotesButtonProps>(
|
|||
noteIds={noteIds}
|
||||
showNotes={showNotes}
|
||||
size={size}
|
||||
status={status}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
text={text}
|
||||
updateNote={updateNote}
|
||||
timelineType={timelineType}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
|
||||
import {
|
||||
mockGlobalState,
|
||||
apolloClientObservable,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
createSecuritySolutionStorageMock,
|
||||
TestProviders,
|
||||
kibanaObservable,
|
||||
} from '../../../../common/mock';
|
||||
import { createStore, State } from '../../../../common/store';
|
||||
import { useThrottledResizeObserver } from '../../../../common/components/utils';
|
||||
|
@ -86,6 +87,7 @@ const defaultProps = {
|
|||
isDatepickerLocked: false,
|
||||
isFavorite: false,
|
||||
title: '',
|
||||
timelineType: TimelineType.default,
|
||||
description: '',
|
||||
getNotesByIds: jest.fn(),
|
||||
noteIds: [],
|
||||
|
@ -103,11 +105,23 @@ describe('Properties', () => {
|
|||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let mockedWidth = 1000;
|
||||
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
(useThrottledResizeObserver as jest.Mock).mockReturnValue({ width: mockedWidth });
|
||||
});
|
||||
|
||||
|
@ -130,9 +144,10 @@ describe('Properties', () => {
|
|||
});
|
||||
|
||||
test('renders correctly draft timeline', () => {
|
||||
const testProps = { ...defaultProps, status: TimelineStatus.draft };
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, status: TimelineStatus.draft }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -157,9 +172,11 @@ describe('Properties', () => {
|
|||
});
|
||||
|
||||
test('it renders a filled star icon when it is a favorite', () => {
|
||||
const testProps = { ...defaultProps, isFavorite: true };
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, isFavorite: true }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -168,10 +185,10 @@ describe('Properties', () => {
|
|||
|
||||
test('it renders the title of the timeline', () => {
|
||||
const title = 'foozle';
|
||||
|
||||
const testProps = { ...defaultProps, title };
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, title }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -194,9 +211,11 @@ describe('Properties', () => {
|
|||
});
|
||||
|
||||
test('it renders the lock icon when isDatepickerLocked is true', () => {
|
||||
const testProps = { ...defaultProps, isDatepickerLocked: true };
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, isDatepickerLocked: true }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
|
@ -223,13 +242,16 @@ describe('Properties', () => {
|
|||
|
||||
test('it renders a description on the left when the width is at least as wide as the threshold', () => {
|
||||
const description = 'strange';
|
||||
const testProps = { ...defaultProps, description };
|
||||
|
||||
// mockedWidth = showDescriptionThreshold;
|
||||
|
||||
(useThrottledResizeObserver as jest.Mock).mockReset();
|
||||
(useThrottledResizeObserver as jest.Mock).mockReturnValue({ width: showDescriptionThreshold });
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, description }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -244,6 +266,9 @@ describe('Properties', () => {
|
|||
|
||||
test('it does NOT render a description on the left when the width is less than the threshold', () => {
|
||||
const description = 'strange';
|
||||
const testProps = { ...defaultProps, description };
|
||||
|
||||
// mockedWidth = showDescriptionThreshold - 1;
|
||||
|
||||
(useThrottledResizeObserver as jest.Mock).mockReset();
|
||||
(useThrottledResizeObserver as jest.Mock).mockReturnValue({
|
||||
|
@ -252,7 +277,7 @@ describe('Properties', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, description }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -313,10 +338,11 @@ describe('Properties', () => {
|
|||
|
||||
test('it renders an avatar for the current user viewing the timeline when it has a title', () => {
|
||||
const title = 'port scan';
|
||||
const testProps = { ...defaultProps, title };
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, title }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -334,9 +360,11 @@ describe('Properties', () => {
|
|||
});
|
||||
|
||||
test('insert timeline - new case', async () => {
|
||||
const testProps = { ...defaultProps, title: 'coolness' };
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, title: 'coolness' }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click');
|
||||
|
@ -352,9 +380,11 @@ describe('Properties', () => {
|
|||
});
|
||||
|
||||
test('insert timeline - existing case', async () => {
|
||||
const testProps = { ...defaultProps, title: 'coolness' };
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Properties {...{ ...defaultProps, title: 'coolness' }} />
|
||||
<Properties {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click');
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { TimelineStatus, TimelineTypeLiteral } from '../../../../../common/types/timeline';
|
||||
import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline';
|
||||
import { useThrottledResizeObserver } from '../../../../common/components/utils';
|
||||
import { Note } from '../../../../common/lib/note';
|
||||
import { InputsModelId } from '../../../../common/store/inputs/constants';
|
||||
|
@ -52,7 +52,8 @@ interface Props {
|
|||
isFavorite: boolean;
|
||||
noteIds: string[];
|
||||
timelineId: string;
|
||||
status: TimelineStatus;
|
||||
timelineType: TimelineTypeLiteral;
|
||||
status: TimelineStatusLiteral;
|
||||
title: string;
|
||||
toggleLock: ToggleLock;
|
||||
updateDescription: UpdateDescription;
|
||||
|
@ -87,6 +88,7 @@ export const Properties = React.memo<Props>(
|
|||
noteIds,
|
||||
status,
|
||||
timelineId,
|
||||
timelineType,
|
||||
title,
|
||||
toggleLock,
|
||||
updateDescription,
|
||||
|
@ -164,10 +166,12 @@ export const Properties = React.memo<Props>(
|
|||
isFavorite={isFavorite}
|
||||
noteIds={noteIds}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
status={status}
|
||||
showDescription={width >= showDescriptionThreshold}
|
||||
showNotes={showNotes}
|
||||
showNotesFromWidth={width >= showNotesThreshold}
|
||||
timelineId={timelineId}
|
||||
timelineType={timelineType}
|
||||
title={title}
|
||||
toggleLock={onToggleLock}
|
||||
updateDescription={updateDescription}
|
||||
|
@ -196,6 +200,7 @@ export const Properties = React.memo<Props>(
|
|||
showUsersView={title.length > 0}
|
||||
status={status}
|
||||
timelineId={timelineId}
|
||||
timelineType={timelineType}
|
||||
title={title}
|
||||
updateDescription={updateDescription}
|
||||
updateNote={updateNote}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
mockGlobalState,
|
||||
apolloClientObservable,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
} from '../../../../common/mock';
|
||||
import { createStore, State } from '../../../../common/store';
|
||||
|
@ -26,7 +27,13 @@ jest.mock('../../../../common/lib/kibana', () => {
|
|||
describe('NewTemplateTimeline', () => {
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
const store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
const mockClosePopover = jest.fn();
|
||||
const mockTitle = 'NEW_TIMELINE';
|
||||
let wrapper: ReactWrapper;
|
||||
|
|
|
@ -10,9 +10,12 @@ import React from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { Description, Name, NotesButton, StarIcon } from './helpers';
|
||||
import { AssociateNote, UpdateNote } from '../../notes/helpers';
|
||||
|
||||
import { Note } from '../../../../common/lib/note';
|
||||
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
|
||||
|
||||
import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void;
|
||||
|
@ -22,6 +25,7 @@ type UpdateDescription = ({ id, description }: { id: string; description: string
|
|||
interface Props {
|
||||
isFavorite: boolean;
|
||||
timelineId: string;
|
||||
timelineType: TimelineTypeLiteral;
|
||||
updateIsFavorite: UpdateIsFavorite;
|
||||
showDescription: boolean;
|
||||
description: string;
|
||||
|
@ -29,6 +33,7 @@ interface Props {
|
|||
updateTitle: UpdateTitle;
|
||||
updateDescription: UpdateDescription;
|
||||
showNotes: boolean;
|
||||
status: TimelineStatusLiteral;
|
||||
associateNote: AssociateNote;
|
||||
showNotesFromWidth: boolean;
|
||||
getNotesByIds: (noteIds: string[]) => Note[];
|
||||
|
@ -77,8 +82,10 @@ export const PropertiesLeft = React.memo<Props>(
|
|||
showDescription,
|
||||
description,
|
||||
title,
|
||||
timelineType,
|
||||
updateTitle,
|
||||
updateDescription,
|
||||
status,
|
||||
showNotes,
|
||||
showNotesFromWidth,
|
||||
associateNote,
|
||||
|
@ -120,10 +127,12 @@ export const PropertiesLeft = React.memo<Props>(
|
|||
noteIds={noteIds}
|
||||
showNotes={showNotes}
|
||||
size="l"
|
||||
status={status}
|
||||
text={i18n.NOTES}
|
||||
toggleShowNotes={onToggleShowNotes}
|
||||
toolTip={i18n.NOTES_TOOL_TIP}
|
||||
updateNote={updateNote}
|
||||
timelineType={timelineType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
|
||||
import { PropertiesRight } from './properties_right';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
|
||||
import { disableTemplate } from '../../../../../common/constants';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
|
@ -67,6 +67,7 @@ describe('Properties Right', () => {
|
|||
onOpenTimelineModal: jest.fn(),
|
||||
status: TimelineStatus.active,
|
||||
showTimelineModal: false,
|
||||
timelineType: TimelineType.default,
|
||||
title: 'title',
|
||||
updateNote: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { NewTimeline, Description, NotesButton, NewCase, ExistingCase } from './helpers';
|
||||
|
||||
import { disableTemplate } from '../../../../../common/constants';
|
||||
import { TimelineStatus } from '../../../../../common/types/timeline';
|
||||
import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline';
|
||||
|
||||
import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -83,9 +83,10 @@ interface PropertiesRightComponentProps {
|
|||
showNotesFromWidth: boolean;
|
||||
showTimelineModal: boolean;
|
||||
showUsersView: boolean;
|
||||
status: TimelineStatus;
|
||||
status: TimelineStatusLiteral;
|
||||
timelineId: string;
|
||||
title: string;
|
||||
timelineType: TimelineTypeLiteral;
|
||||
updateDescription: UpdateDescription;
|
||||
updateNote: UpdateNote;
|
||||
usersViewing: string[];
|
||||
|
@ -111,6 +112,7 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
|
|||
showTimelineModal,
|
||||
showUsersView,
|
||||
status,
|
||||
timelineType,
|
||||
timelineId,
|
||||
title,
|
||||
updateDescription,
|
||||
|
@ -203,6 +205,8 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
|
|||
noteIds={noteIds}
|
||||
showNotes={showNotes}
|
||||
size="l"
|
||||
status={status}
|
||||
timelineType={timelineType}
|
||||
text={i18n.NOTES}
|
||||
toggleShowNotes={onToggleShowNotes}
|
||||
toolTip={i18n.NOTES_TOOL_TIP}
|
||||
|
|
|
@ -112,7 +112,7 @@ export const NEW_TIMELINE = i18n.translate(
|
|||
export const NEW_TEMPLATE_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.properties.newTemplateTimelineButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create template timeline',
|
||||
defaultMessage: 'Create new timeline template',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -30,11 +30,7 @@ describe('SelectableTimeline', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const {
|
||||
SelectableTimeline,
|
||||
|
||||
ORIGINAL_PAGE_SIZE,
|
||||
} = jest.requireActual('./');
|
||||
const { SelectableTimeline, ORIGINAL_PAGE_SIZE } = jest.requireActual('./');
|
||||
|
||||
const props = {
|
||||
hideUntitled: false,
|
||||
|
@ -94,8 +90,10 @@ describe('SelectableTimeline', () => {
|
|||
sortField: SortFieldTimeline.updated,
|
||||
sortOrder: Direction.desc,
|
||||
},
|
||||
status: null,
|
||||
onlyUserFavorite: false,
|
||||
timelineType: TimelineType.default,
|
||||
templateTimelineType: null,
|
||||
};
|
||||
beforeAll(() => {
|
||||
mount(<SelectableTimeline {...props} />);
|
||||
|
|
|
@ -33,6 +33,7 @@ import * as i18nTimeline from '../../open_timeline/translations';
|
|||
import { OpenTimelineResult } from '../../open_timeline/types';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
import * as i18n from '../translations';
|
||||
import { useTimelineStatus } from '../../open_timeline/use_timeline_status';
|
||||
|
||||
const MyEuiFlexItem = styled(EuiFlexItem)`
|
||||
display: inline-block;
|
||||
|
@ -118,6 +119,7 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
|
|||
const [onlyFavorites, setOnlyFavorites] = useState(false);
|
||||
const [searchRef, setSearchRef] = useState<HTMLElement | null>(null);
|
||||
const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline();
|
||||
const { timelineStatus, templateTimelineType } = useTimelineStatus({ timelineType });
|
||||
|
||||
const onSearchTimeline = useCallback((val) => {
|
||||
setSearchTimelineValue(val);
|
||||
|
@ -249,8 +251,7 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
|
|||
},
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
useEffect(() => {
|
||||
fetchAllTimeline({
|
||||
pageInfo: {
|
||||
pageIndex: 1,
|
||||
|
@ -262,11 +263,19 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
|
|||
sortOrder: Direction.desc,
|
||||
},
|
||||
onlyUserFavorite: onlyFavorites,
|
||||
status: timelineStatus,
|
||||
timelineType,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[onlyFavorites, pageSize, searchTimelineValue, timelineType]
|
||||
);
|
||||
templateTimelineType,
|
||||
});
|
||||
}, [
|
||||
fetchAllTimeline,
|
||||
onlyFavorites,
|
||||
pageSize,
|
||||
searchTimelineValue,
|
||||
timelineType,
|
||||
timelineStatus,
|
||||
templateTimelineType,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EuiSelectableContainer isLoading={loading}>
|
||||
|
|
|
@ -24,6 +24,7 @@ import { TimelineComponent, Props as TimelineComponentProps } from './timeline';
|
|||
import { Sort } from './body/sort';
|
||||
import { mockDataProviders } from './data_providers/mock/mock_data_providers';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
import { TimelineStatus } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('./properties/properties_right');
|
||||
|
@ -96,6 +97,7 @@ describe('Timeline', () => {
|
|||
showCallOutUnauthorizedMsg: false,
|
||||
start: startDate,
|
||||
sort,
|
||||
status: TimelineStatus.active,
|
||||
toggleColumn: jest.fn(),
|
||||
usersViewing: ['elastic'],
|
||||
};
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
IIndexPattern,
|
||||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { useManageTimeline } from '../manage_timeline';
|
||||
import { TimelineStatusLiteral } from '../../../../common/types/timeline';
|
||||
|
||||
const TimelineContainer = styled.div`
|
||||
height: 100%;
|
||||
|
@ -110,6 +111,7 @@ export interface Props {
|
|||
showCallOutUnauthorizedMsg: boolean;
|
||||
start: number;
|
||||
sort: Sort;
|
||||
status: TimelineStatusLiteral;
|
||||
toggleColumn: (column: ColumnHeaderOptions) => void;
|
||||
usersViewing: string[];
|
||||
}
|
||||
|
@ -141,6 +143,7 @@ export const TimelineComponent: React.FC<Props> = ({
|
|||
show,
|
||||
showCallOutUnauthorizedMsg,
|
||||
start,
|
||||
status,
|
||||
sort,
|
||||
toggleColumn,
|
||||
usersViewing,
|
||||
|
@ -214,6 +217,7 @@ export const TimelineComponent: React.FC<Props> = ({
|
|||
onToggleDataProviderExcluded={onToggleDataProviderExcluded}
|
||||
show={show}
|
||||
showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg}
|
||||
status={status}
|
||||
/>
|
||||
</TimelineHeaderContainer>
|
||||
</StyledEuiFlyoutHeader>
|
||||
|
|
|
@ -13,6 +13,8 @@ export const allTimelinesQuery = gql`
|
|||
$sort: SortTimeline
|
||||
$onlyUserFavorite: Boolean
|
||||
$timelineType: TimelineType
|
||||
$templateTimelineType: TemplateTimelineType
|
||||
$status: TimelineStatus
|
||||
) {
|
||||
getAllTimeline(
|
||||
pageInfo: $pageInfo
|
||||
|
@ -20,8 +22,15 @@ export const allTimelinesQuery = gql`
|
|||
sort: $sort
|
||||
onlyUserFavorite: $onlyUserFavorite
|
||||
timelineType: $timelineType
|
||||
templateTimelineType: $templateTimelineType
|
||||
status: $status
|
||||
) {
|
||||
totalCount
|
||||
defaultTimelineCount
|
||||
templateTimelineCount
|
||||
elasticTemplateTimelineCount
|
||||
customTemplateTimelineCount
|
||||
favoriteCount
|
||||
timeline {
|
||||
savedObjectId
|
||||
description
|
||||
|
|
|
@ -22,7 +22,11 @@ import { useApolloClient } from '../../../common/utils/apollo_context';
|
|||
|
||||
import { allTimelinesQuery } from './index.gql_query';
|
||||
import * as i18n from '../../pages/translations';
|
||||
import { TimelineTypeLiteralWithNull } from '../../../../common/types/timeline';
|
||||
import {
|
||||
TimelineTypeLiteralWithNull,
|
||||
TimelineStatusLiteralWithNull,
|
||||
TemplateTimelineTypeLiteralWithNull,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
export interface AllTimelinesArgs {
|
||||
fetchAllTimeline: ({
|
||||
|
@ -30,11 +34,17 @@ export interface AllTimelinesArgs {
|
|||
pageInfo,
|
||||
search,
|
||||
sort,
|
||||
status,
|
||||
timelineType,
|
||||
}: AllTimelinesVariables) => void;
|
||||
timelines: OpenTimelineResult[];
|
||||
loading: boolean;
|
||||
totalCount: number;
|
||||
customTemplateTimelineCount: number;
|
||||
defaultTimelineCount: number;
|
||||
elasticTemplateTimelineCount: number;
|
||||
templateTimelineCount: number;
|
||||
favoriteCount: number;
|
||||
}
|
||||
|
||||
export interface AllTimelinesVariables {
|
||||
|
@ -42,7 +52,9 @@ export interface AllTimelinesVariables {
|
|||
pageInfo: PageInfoTimeline;
|
||||
search: string;
|
||||
sort: SortTimeline;
|
||||
status: TimelineStatusLiteralWithNull;
|
||||
timelineType: TimelineTypeLiteralWithNull;
|
||||
templateTimelineType: TemplateTimelineTypeLiteralWithNull;
|
||||
}
|
||||
|
||||
export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES';
|
||||
|
@ -76,6 +88,7 @@ export const getAllTimeline = memoizeOne(
|
|||
)
|
||||
: null,
|
||||
savedObjectId: timeline.savedObjectId,
|
||||
status: timeline.status,
|
||||
title: timeline.title,
|
||||
updated: timeline.updated,
|
||||
updatedBy: timeline.updatedBy,
|
||||
|
@ -90,27 +103,39 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
|
|||
loading: false,
|
||||
totalCount: 0,
|
||||
timelines: [],
|
||||
customTemplateTimelineCount: 0,
|
||||
defaultTimelineCount: 0,
|
||||
elasticTemplateTimelineCount: 0,
|
||||
templateTimelineCount: 0,
|
||||
favoriteCount: 0,
|
||||
});
|
||||
|
||||
const fetchAllTimeline = useCallback(
|
||||
({ onlyUserFavorite, pageInfo, search, sort, timelineType }: AllTimelinesVariables) => {
|
||||
async ({
|
||||
onlyUserFavorite,
|
||||
pageInfo,
|
||||
search,
|
||||
sort,
|
||||
status,
|
||||
timelineType,
|
||||
templateTimelineType,
|
||||
}: AllTimelinesVariables) => {
|
||||
let didCancel = false;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (apolloClient != null) {
|
||||
setAllTimelines({
|
||||
...allTimelines,
|
||||
loading: true,
|
||||
});
|
||||
setAllTimelines((prevState) => ({ ...prevState, loading: true }));
|
||||
|
||||
const variables: GetAllTimeline.Variables = {
|
||||
onlyUserFavorite,
|
||||
pageInfo,
|
||||
search,
|
||||
sort,
|
||||
status,
|
||||
timelineType,
|
||||
templateTimelineType,
|
||||
};
|
||||
const response = await apolloClient.query<
|
||||
GetAllTimeline.Query,
|
||||
|
@ -125,8 +150,16 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
|
|||
},
|
||||
},
|
||||
});
|
||||
const totalCount = response?.data?.getAllTimeline?.totalCount ?? 0;
|
||||
const timelines = response?.data?.getAllTimeline?.timeline ?? [];
|
||||
const getAllTimelineResponse = response?.data?.getAllTimeline;
|
||||
const totalCount = getAllTimelineResponse?.totalCount ?? 0;
|
||||
const timelines = getAllTimelineResponse?.timeline ?? [];
|
||||
const customTemplateTimelineCount =
|
||||
getAllTimelineResponse?.customTemplateTimelineCount ?? 0;
|
||||
const defaultTimelineCount = getAllTimelineResponse?.defaultTimelineCount ?? 0;
|
||||
const elasticTemplateTimelineCount =
|
||||
getAllTimelineResponse?.elasticTemplateTimelineCount ?? 0;
|
||||
const templateTimelineCount = getAllTimelineResponse?.templateTimelineCount ?? 0;
|
||||
const favoriteCount = getAllTimelineResponse?.favoriteCount ?? 0;
|
||||
if (!didCancel) {
|
||||
dispatch(
|
||||
inputsActions.setQuery({
|
||||
|
@ -141,6 +174,11 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
|
|||
loading: false,
|
||||
totalCount,
|
||||
timelines: getAllTimeline(JSON.stringify(variables), timelines as TimelineResult[]),
|
||||
customTemplateTimelineCount,
|
||||
defaultTimelineCount,
|
||||
elasticTemplateTimelineCount,
|
||||
templateTimelineCount,
|
||||
favoriteCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -155,6 +193,11 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
|
|||
loading: false,
|
||||
totalCount: 0,
|
||||
timelines: [],
|
||||
customTemplateTimelineCount: 0,
|
||||
defaultTimelineCount: 0,
|
||||
elasticTemplateTimelineCount: 0,
|
||||
templateTimelineCount: 0,
|
||||
favoriteCount: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +208,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
|
|||
abortCtrl.abort();
|
||||
};
|
||||
},
|
||||
[apolloClient, allTimelines, dispatch, dispatchToaster]
|
||||
[apolloClient, dispatch, dispatchToaster]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -165,6 +165,7 @@ describe('persistTimeline', () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
const version = null;
|
||||
const fetchMock = jest.fn();
|
||||
const postMock = jest.fn();
|
||||
|
@ -180,7 +181,11 @@ describe('persistTimeline', () => {
|
|||
patch: patchMock.mockReturnValue(mockPatchTimelineResponse),
|
||||
},
|
||||
});
|
||||
api.persistTimeline({ timelineId, timeline: initialDraftTimeline, version });
|
||||
api.persistTimeline({
|
||||
timelineId,
|
||||
timeline: initialDraftTimeline,
|
||||
version,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
TimelineResponse,
|
||||
TimelineResponseType,
|
||||
TimelineStatus,
|
||||
TimelineErrorResponseType,
|
||||
TimelineErrorResponse,
|
||||
} from '../../../common/types/timeline';
|
||||
import { TimelineInput, TimelineType } from '../../graphql/types';
|
||||
import {
|
||||
|
@ -48,6 +50,12 @@ const decodeTimelineResponse = (respTimeline?: TimelineResponse) =>
|
|||
fold(throwErrors(createToasterPlainError), identity)
|
||||
);
|
||||
|
||||
const decodeTimelineErrorResponse = (respTimeline?: TimelineErrorResponse) =>
|
||||
pipe(
|
||||
TimelineErrorResponseType.decode(respTimeline),
|
||||
fold(throwErrors(createToasterPlainError), identity)
|
||||
);
|
||||
|
||||
const postTimeline = async ({ timeline }: RequestPostTimeline): Promise<TimelineResponse> => {
|
||||
const response = await KibanaServices.get().http.post<TimelineResponse>(TIMELINE_URL, {
|
||||
method: 'POST',
|
||||
|
@ -61,12 +69,19 @@ const patchTimeline = async ({
|
|||
timelineId,
|
||||
timeline,
|
||||
version,
|
||||
}: RequestPatchTimeline): Promise<TimelineResponse> => {
|
||||
const response = await KibanaServices.get().http.patch<TimelineResponse>(TIMELINE_URL, {
|
||||
}: RequestPatchTimeline): Promise<TimelineResponse | TimelineErrorResponse> => {
|
||||
let response = null;
|
||||
try {
|
||||
response = await KibanaServices.get().http.patch<TimelineResponse>(TIMELINE_URL, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ timeline, timelineId, version }),
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
// For Future developer
|
||||
// We are not rejecting our promise here because we had issue with our RXJS epic
|
||||
// the issue we were not able to pass the right object to it so we did manage the error in the success
|
||||
return Promise.resolve(decodeTimelineErrorResponse(err.body));
|
||||
}
|
||||
return decodeTimelineResponse(response);
|
||||
};
|
||||
|
||||
|
@ -74,17 +89,31 @@ export const persistTimeline = async ({
|
|||
timelineId,
|
||||
timeline,
|
||||
version,
|
||||
}: RequestPersistTimeline): Promise<TimelineResponse> => {
|
||||
if (timelineId == null && timeline.status === TimelineStatus.draft) {
|
||||
const draftTimeline = await cleanDraftTimeline({ timelineType: timeline.timelineType! });
|
||||
}: RequestPersistTimeline): Promise<TimelineResponse | TimelineErrorResponse> => {
|
||||
if (timelineId == null && timeline.status === TimelineStatus.draft && timeline) {
|
||||
const draftTimeline = await cleanDraftTimeline({
|
||||
timelineType: timeline.timelineType!,
|
||||
templateTimelineId: timeline.templateTimelineId ?? undefined,
|
||||
templateTimelineVersion: timeline.templateTimelineVersion ?? undefined,
|
||||
});
|
||||
|
||||
const templateTimelineInfo =
|
||||
timeline.timelineType! === TimelineType.template
|
||||
? {
|
||||
templateTimelineId:
|
||||
draftTimeline.data.persistTimeline.timeline.templateTimelineId ??
|
||||
timeline.templateTimelineId,
|
||||
templateTimelineVersion:
|
||||
draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ??
|
||||
timeline.templateTimelineVersion,
|
||||
}
|
||||
: {};
|
||||
|
||||
return patchTimeline({
|
||||
timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId,
|
||||
timeline: {
|
||||
...timeline,
|
||||
templateTimelineId: draftTimeline.data.persistTimeline.timeline.templateTimelineId,
|
||||
templateTimelineVersion:
|
||||
draftTimeline.data.persistTimeline.timeline.templateTimelineVersion,
|
||||
...templateTimelineInfo,
|
||||
},
|
||||
version: draftTimeline.data.persistTimeline.timeline.version ?? '',
|
||||
});
|
||||
|
@ -147,12 +176,24 @@ export const getDraftTimeline = async ({
|
|||
|
||||
export const cleanDraftTimeline = async ({
|
||||
timelineType,
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
}: {
|
||||
timelineType: TimelineType;
|
||||
templateTimelineId?: string;
|
||||
templateTimelineVersion?: number;
|
||||
}): Promise<TimelineResponse> => {
|
||||
const templateTimelineInfo =
|
||||
timelineType === TimelineType.template
|
||||
? {
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
}
|
||||
: {};
|
||||
const response = await KibanaServices.get().http.post<TimelineResponse>(TIMELINE_DRAFT_URL, {
|
||||
body: JSON.stringify({
|
||||
timelineType,
|
||||
...templateTimelineInfo,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -30,3 +30,17 @@ export const ERROR_FETCHING_TIMELINES_TITLE = i18n.translate(
|
|||
defaultMessage: 'Failed to query all timelines data',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_TIMELINE_ERROR_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timelines.updateTimelineErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Timeline error',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_TIMELINE_ERROR_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.timelines.updateTimelineErrorText',
|
||||
{
|
||||
defaultMessage: 'Something went wrong',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -70,6 +70,8 @@ export const createTimeline = actionCreator<{
|
|||
showCheckboxes?: boolean;
|
||||
showRowRenderers?: boolean;
|
||||
timelineType?: TimelineTypeLiteral;
|
||||
templateTimelineId?: string;
|
||||
templateTimelineVersion?: number;
|
||||
}>('CREATE_TIMELINE');
|
||||
|
||||
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
Filter,
|
||||
MatchAllFilter,
|
||||
} from '../../../../../../.../../../src/plugins/data/public';
|
||||
import { TimelineStatus } from '../../../../common/types/timeline';
|
||||
import { TimelineStatus, TimelineErrorResponse } from '../../../../common/types/timeline';
|
||||
import { inputsModel } from '../../../common/store/inputs';
|
||||
import {
|
||||
TimelineType,
|
||||
|
@ -43,6 +43,10 @@ import {
|
|||
} from '../../../graphql/types';
|
||||
import { addError } from '../../../common/store/app/actions';
|
||||
|
||||
import { persistTimeline } from '../../containers/api';
|
||||
import { ALL_TIMELINE_QUERY_ID } from '../../containers/all';
|
||||
import * as i18n from '../../pages/translations';
|
||||
|
||||
import {
|
||||
applyKqlFilterQuery,
|
||||
addProvider,
|
||||
|
@ -79,8 +83,6 @@ import { isNotNull } from './helpers';
|
|||
import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue';
|
||||
import { myEpicTimelineId } from './my_epic_timeline_id';
|
||||
import { ActionTimeline, TimelineEpicDependencies } from './types';
|
||||
import { persistTimeline } from '../../containers/api';
|
||||
import { ALL_TIMELINE_QUERY_ID } from '../../containers/all';
|
||||
|
||||
const timelineActionsType = [
|
||||
applyKqlFilterQuery.type,
|
||||
|
@ -121,6 +123,7 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
timelineByIdSelector,
|
||||
timelineTimeRangeSelector,
|
||||
apolloClient$,
|
||||
kibana$,
|
||||
}
|
||||
) => {
|
||||
const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull));
|
||||
|
@ -146,13 +149,24 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
if (action.type === addError.type) {
|
||||
return true;
|
||||
}
|
||||
if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) {
|
||||
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 true;
|
||||
} else if (
|
||||
timelineActionsType.includes(action.type) &&
|
||||
|
@ -176,6 +190,8 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
const action: ActionTimeline = get('action', objAction);
|
||||
const timelineId = myEpicTimelineId.getTimelineId();
|
||||
const version = myEpicTimelineId.getTimelineVersion();
|
||||
const templateTimelineId = myEpicTimelineId.getTemplateTimelineId();
|
||||
const templateTimelineVersion = myEpicTimelineId.getTemplateTimelineVersion();
|
||||
|
||||
if (timelineNoteActionsType.includes(action.type)) {
|
||||
return epicPersistNote(
|
||||
|
@ -211,13 +227,37 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
persistTimeline({
|
||||
timelineId,
|
||||
version,
|
||||
timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
|
||||
timeline: {
|
||||
...convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
},
|
||||
})
|
||||
).pipe(
|
||||
withLatestFrom(timeline$, allTimelineQuery$),
|
||||
mergeMap(([result, recentTimeline, allTimelineQuery]) => {
|
||||
withLatestFrom(timeline$, allTimelineQuery$, kibana$),
|
||||
mergeMap(([result, recentTimeline, allTimelineQuery, kibana]) => {
|
||||
const error = result as TimelineErrorResponse;
|
||||
if (error.status_code != null && error.status_code === 405) {
|
||||
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 savedTimeline = recentTimeline[action.payload.id];
|
||||
const response: ResponseTimeline = get('data.persistTimeline', result);
|
||||
if (response == null) {
|
||||
return [
|
||||
endTimelineSaving({
|
||||
id: action.payload.id,
|
||||
}),
|
||||
];
|
||||
}
|
||||
const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
|
||||
|
||||
if (allTimelineQuery.refetch != null) {
|
||||
|
@ -264,6 +304,12 @@ export const createTimelineEpic = <State>(): Epic<
|
|||
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;
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
defaultHeaders,
|
||||
createSecuritySolutionStorageMock,
|
||||
mockIndexPattern,
|
||||
kibanaObservable,
|
||||
} from '../../../common/mock';
|
||||
|
||||
import { createStore, State } from '../../../common/store';
|
||||
|
@ -38,6 +39,7 @@ import { Direction } from '../../../graphql/types';
|
|||
|
||||
import { addTimelineInStorage } from '../../containers/local_storage';
|
||||
import { isPageTimeline } from './epic_local_storage';
|
||||
import { TimelineStatus } from '../../../../common/types/timeline';
|
||||
|
||||
jest.mock('../../containers/local_storage');
|
||||
|
||||
|
@ -50,7 +52,13 @@ const addTimelineInStorageMock = addTimelineInStorage as jest.Mock;
|
|||
describe('epicLocalStorage', () => {
|
||||
const state: State = mockGlobalState;
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
let store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
|
||||
let props = {} as TimelineComponentProps;
|
||||
const sort: Sort = {
|
||||
|
@ -63,7 +71,13 @@ describe('epicLocalStorage', () => {
|
|||
const indexPattern = mockIndexPattern;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
|
||||
store = createStore(
|
||||
state,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
apolloClientObservable,
|
||||
kibanaObservable,
|
||||
storage
|
||||
);
|
||||
props = {
|
||||
browserFields: mockBrowserFields,
|
||||
columns: defaultHeaders,
|
||||
|
@ -89,6 +103,7 @@ describe('epicLocalStorage', () => {
|
|||
show: true,
|
||||
showCallOutUnauthorizedMsg: false,
|
||||
start: startDate,
|
||||
status: TimelineStatus.active,
|
||||
sort,
|
||||
toggleColumn: jest.fn(),
|
||||
usersViewing: ['elastic'],
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { Filter } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
import { disableTemplate } from '../../../../common/constants';
|
||||
|
@ -19,7 +20,7 @@ import {
|
|||
} from '../../../timelines/components/timeline/data_providers/data_provider';
|
||||
import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model';
|
||||
import { TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { TimelineTypeLiteral } from '../../../../common/types/timeline';
|
||||
import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline';
|
||||
|
||||
import { timelineDefaults } from './defaults';
|
||||
import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model';
|
||||
|
@ -158,7 +159,15 @@ export const addNewTimeline = ({
|
|||
showRowRenderers = true,
|
||||
timelineById,
|
||||
timelineType,
|
||||
}: AddNewTimelineParams): TimelineById => ({
|
||||
}: AddNewTimelineParams): TimelineById => {
|
||||
const templateTimelineInfo =
|
||||
!disableTemplate && timelineType === TimelineType.template
|
||||
? {
|
||||
templateTimelineId: uuid.v4(),
|
||||
templateTimelineVersion: 1,
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
...timelineById,
|
||||
[id]: {
|
||||
id,
|
||||
|
@ -178,8 +187,10 @@ export const addNewTimeline = ({
|
|||
showCheckboxes,
|
||||
showRowRenderers,
|
||||
timelineType: !disableTemplate ? timelineType : timelineDefaults.timelineType,
|
||||
...templateTimelineInfo,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
interface PinTimelineEventParams {
|
||||
id: string;
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
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;
|
||||
|
@ -16,6 +18,14 @@ export class ManageEpicTimelineId {
|
|||
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;
|
||||
}
|
||||
|
@ -23,4 +33,12 @@ export class ManageEpicTimelineId {
|
|||
public setTimelineVersion(version: string | null) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public setTemplateTimelineId(templateTimelineId: string | null) {
|
||||
this.templateTimelineId = templateTimelineId;
|
||||
}
|
||||
|
||||
public setTemplateTimelineVersion(templateVersion: number | null) {
|
||||
this.templateVersion = templateVersion;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,7 +160,6 @@ export type SubsetTimelineModel = Readonly<
|
|||
| 'isLoading'
|
||||
| 'savedObjectId'
|
||||
| 'version'
|
||||
| 'timelineType'
|
||||
| 'status'
|
||||
>
|
||||
>;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue