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