mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Notes] - store setup (#186433)
This commit is contained in:
parent
6ce61db2ff
commit
153ec668e3
10 changed files with 228 additions and 8 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1445,6 +1445,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
|
|||
/x-pack/plugins/security_solution/public/detections/components/alerts_info @elastic/security-threat-hunting-investigations
|
||||
/x-pack/plugins/security_solution/public/flyout/document_details @elastic/security-threat-hunting-investigations
|
||||
/x-pack/plugins/security_solution/public/flyout/shared @elastic/security-threat-hunting-investigations
|
||||
/x-pack/plugins/security_solution/public/notes @elastic/security-threat-hunting-investigations
|
||||
/x-pack/plugins/security_solution/public/resolver @elastic/security-threat-hunting-investigations
|
||||
/x-pack/plugins/security_solution/public/threat_intelligence @elastic/security-threat-hunting-investigations
|
||||
/x-pack/plugins/security_solution/public/timelines @elastic/security-threat-hunting-investigations
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import type { DataViewSpec, FieldSpec } from '@kbn/data-views-plugin/public';
|
||||
import { ReqStatus } from '../../notes/store/notes.slice';
|
||||
import { HostsFields } from '../../../common/api/search_strategy/hosts/model/sort';
|
||||
import { InputsModelId } from '../store/inputs/constants';
|
||||
import {
|
||||
|
@ -500,4 +501,26 @@ export const mockGlobalState: State = {
|
|||
*/
|
||||
management: mockManagementState as ManagementState,
|
||||
discover: getMockDiscoverInTimelineState(),
|
||||
notes: {
|
||||
ids: ['1'],
|
||||
entities: {
|
||||
'1': {
|
||||
eventId: 'event-id',
|
||||
noteId: '1',
|
||||
note: 'note-1',
|
||||
timelineId: 'timeline-1',
|
||||
created: 1663882629000,
|
||||
createdBy: 'elastic',
|
||||
updated: 1663882629000,
|
||||
updatedBy: 'elastic',
|
||||
version: 'version',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
fetchNotesByDocumentId: ReqStatus.Idle,
|
||||
},
|
||||
error: {
|
||||
fetchNotesByDocumentId: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useSourcererDataView } from '../../sourcerer/containers';
|
|||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { initialGroupingState } from './grouping/reducer';
|
||||
import { initialAnalyzerState } from '../../resolver/store/helpers';
|
||||
import { initialNotesState } from '../../notes/store/notes.slice';
|
||||
|
||||
jest.mock('../hooks/use_selector');
|
||||
jest.mock('../lib/kibana', () => {
|
||||
|
@ -69,7 +70,8 @@ describe('createInitialState', () => {
|
|||
},
|
||||
{
|
||||
analyzer: initialAnalyzerState,
|
||||
}
|
||||
},
|
||||
initialNotesState
|
||||
);
|
||||
|
||||
test('indicesExist should be TRUE if patternList is NOT empty', async () => {
|
||||
|
@ -107,7 +109,9 @@ describe('createInitialState', () => {
|
|||
},
|
||||
{
|
||||
analyzer: initialAnalyzerState,
|
||||
}
|
||||
},
|
||||
|
||||
initialNotesState
|
||||
);
|
||||
const { result } = renderHook(() => useSourcererDataView(), {
|
||||
wrapper: ({ children }) => (
|
||||
|
|
|
@ -35,6 +35,8 @@ import type { GroupState } from './grouping/types';
|
|||
import { analyzerReducer } from '../../resolver/store/reducer';
|
||||
import { securitySolutionDiscoverReducer } from './discover/reducer';
|
||||
import type { AnalyzerState } from '../../resolver/types';
|
||||
import type { NotesState } from '../../notes/store/notes.slice';
|
||||
import { notesReducer } from '../../notes/store/notes.slice';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
|
@ -66,7 +68,8 @@ export const createInitialState = (
|
|||
},
|
||||
dataTableState: DataTableState,
|
||||
groupsState: GroupState,
|
||||
analyzerState: AnalyzerState
|
||||
analyzerState: AnalyzerState,
|
||||
notesState: NotesState
|
||||
): State => {
|
||||
const initialPatterns = {
|
||||
[SourcererScopeName.default]: getScopePatternListSelection(
|
||||
|
@ -128,6 +131,7 @@ export const createInitialState = (
|
|||
internal: undefined,
|
||||
savedSearch: undefined,
|
||||
},
|
||||
notes: notesState,
|
||||
};
|
||||
|
||||
return preloadedState;
|
||||
|
@ -150,4 +154,5 @@ export const createReducer: (
|
|||
analyzer: analyzerReducer,
|
||||
discover: securitySolutionDiscoverReducer,
|
||||
...pluginsReducer,
|
||||
notes: notesReducer,
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import thunk from 'redux-thunk';
|
||||
import type {
|
||||
Action,
|
||||
Store,
|
||||
|
@ -54,6 +55,7 @@ import { dataAccessLayerFactory } from '../../resolver/data_access_layer/factory
|
|||
import { sourcererActions } from '../../sourcerer/store';
|
||||
import { createMiddlewares } from './middlewares';
|
||||
import { addNewTimeline } from '../../timelines/store/helpers';
|
||||
import { initialNotesState } from '../../notes/store/notes.slice';
|
||||
|
||||
let store: Store<State, Action> | null = null;
|
||||
|
||||
|
@ -168,7 +170,8 @@ export const createStoreFactory = async (
|
|||
},
|
||||
dataTableInitialState,
|
||||
groupsInitialState,
|
||||
analyzerInitialState
|
||||
analyzerInitialState,
|
||||
initialNotesState
|
||||
);
|
||||
|
||||
const rootReducer = {
|
||||
|
@ -284,7 +287,8 @@ export const createStore = (
|
|||
const middlewareEnhancer = applyMiddleware(
|
||||
...createMiddlewares(kibana, storage),
|
||||
telemetryMiddleware,
|
||||
...(additionalMiddleware ?? [])
|
||||
...(additionalMiddleware ?? []),
|
||||
thunk
|
||||
);
|
||||
|
||||
store = createReduxStore(
|
||||
|
|
|
@ -25,11 +25,11 @@ import type { GlobalUrlParam } from './global_url_param';
|
|||
import type { GroupState } from './grouping/types';
|
||||
import type { SecuritySolutionDiscoverState } from './discover/model';
|
||||
import type { AnalyzerState } from '../../resolver/types';
|
||||
import type { NotesState } from '../../notes/store/notes.slice';
|
||||
|
||||
export type State = HostsPluginState &
|
||||
UsersPluginState &
|
||||
NetworkPluginState &
|
||||
UsersPluginState &
|
||||
TimelinePluginState &
|
||||
ManagementPluginState & {
|
||||
app: AppState;
|
||||
|
@ -40,7 +40,7 @@ export type State = HostsPluginState &
|
|||
discover: SecuritySolutionDiscoverState;
|
||||
} & DataTableState &
|
||||
GroupState &
|
||||
AnalyzerState;
|
||||
AnalyzerState & { notes: NotesState };
|
||||
/**
|
||||
* The Redux store type for the Security app.
|
||||
*/
|
||||
|
|
|
@ -5,13 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { fetchNotesByDocumentId } from '../../../../notes/store/notes.slice';
|
||||
import { useLeftPanelContext } from '../context';
|
||||
|
||||
/**
|
||||
* List all the notes for a document id and allows to create new notes associated with that document.
|
||||
* Displayed in the document details expandable flyout left section.
|
||||
*/
|
||||
export const NotesDetails = memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
const { eventId } = useLeftPanelContext();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchNotesByDocumentId({ documentId: eventId }));
|
||||
}, [dispatch, eventId]);
|
||||
|
||||
return <></>;
|
||||
});
|
||||
|
||||
|
|
33
x-pack/plugins/security_solution/public/notes/api/api.ts
Normal file
33
x-pack/plugins/security_solution/public/notes/api/api.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
// TODO point to the correct API when it is available
|
||||
/**
|
||||
* Fetches all the notes for a document id
|
||||
*/
|
||||
export const fetchNotesByDocumentId = async (documentId: string) => {
|
||||
const response = {
|
||||
totalCount: 1,
|
||||
notes: [generateNoteMock(documentId)],
|
||||
};
|
||||
return response.notes;
|
||||
};
|
||||
|
||||
// TODO remove when the API is available
|
||||
const generateNoteMock = (documentId: string) => ({
|
||||
noteId: uuid.v4(),
|
||||
version: 'WzU1MDEsMV0=',
|
||||
timelineId: '',
|
||||
eventId: documentId,
|
||||
note: 'This is a mocked note',
|
||||
created: new Date().getTime(),
|
||||
createdBy: 'elastic',
|
||||
updated: new Date().getTime(),
|
||||
updatedBy: 'elastic',
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Note } from '../../../common/api/timeline';
|
||||
|
||||
/**
|
||||
* Interface to represent a normalized entity
|
||||
*/
|
||||
export interface NormalizedEntity<T> {
|
||||
entities: {
|
||||
[entity: string]: {
|
||||
[id: string]: T;
|
||||
};
|
||||
};
|
||||
result: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to represent normalized entities
|
||||
*/
|
||||
export interface NormalizedEntities<T> {
|
||||
entities: {
|
||||
[entity: string]: {
|
||||
[id: string]: T;
|
||||
};
|
||||
};
|
||||
result: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a single note
|
||||
*/
|
||||
export const normalizeEntity = (res: Note): NormalizedEntity<Note> => ({
|
||||
entities: {
|
||||
notes: {
|
||||
[res.noteId]: res,
|
||||
},
|
||||
},
|
||||
result: res.noteId,
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalizes an array of notes
|
||||
*/
|
||||
export const normalizeEntities = (res: Note[]): NormalizedEntities<Note> => ({
|
||||
entities: {
|
||||
notes: res.reduce((obj, item) => Object.assign(obj, { [item.noteId]: item }), {}),
|
||||
},
|
||||
result: res.map((note) => note.noteId),
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EntityState, SerializedError } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk, createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
||||
import type { State } from '../../common/store';
|
||||
import { fetchNotesByDocumentId as fetchNotesByDocumentIdApi } from '../api/api';
|
||||
import type { NormalizedEntities } from './normalize';
|
||||
import { normalizeEntities } from './normalize';
|
||||
import type { Note } from '../../../common/api/timeline';
|
||||
|
||||
export enum ReqStatus {
|
||||
Idle = 'idle',
|
||||
Loading = 'loading',
|
||||
Succeeded = 'succeeded',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
interface HttpError {
|
||||
type: 'http';
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface NotesState extends EntityState<Note> {
|
||||
status: {
|
||||
fetchNotesByDocumentId: ReqStatus;
|
||||
};
|
||||
error: {
|
||||
fetchNotesByDocumentId: SerializedError | HttpError | null;
|
||||
};
|
||||
}
|
||||
|
||||
const notesAdapter = createEntityAdapter<Note>({
|
||||
selectId: (note: Note) => note.noteId,
|
||||
});
|
||||
|
||||
export const initialNotesState: NotesState = notesAdapter.getInitialState({
|
||||
status: {
|
||||
fetchNotesByDocumentId: ReqStatus.Idle,
|
||||
},
|
||||
error: {
|
||||
fetchNotesByDocumentId: null,
|
||||
},
|
||||
});
|
||||
|
||||
export const fetchNotesByDocumentId = createAsyncThunk<
|
||||
NormalizedEntities<Note>,
|
||||
{ documentId: string },
|
||||
{}
|
||||
>('notes/fetchNotesByDocumentId', async (args) => {
|
||||
const { documentId } = args;
|
||||
const res = await fetchNotesByDocumentIdApi(documentId);
|
||||
return normalizeEntities(res);
|
||||
});
|
||||
|
||||
const notesSlice = createSlice({
|
||||
name: 'notes',
|
||||
initialState: initialNotesState,
|
||||
reducers: {},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(fetchNotesByDocumentId.pending, (state, action) => {
|
||||
state.status.fetchNotesByDocumentId = ReqStatus.Loading;
|
||||
})
|
||||
.addCase(fetchNotesByDocumentId.fulfilled, (state, action) => {
|
||||
notesAdapter.upsertMany(state, action.payload.entities.notes);
|
||||
state.status.fetchNotesByDocumentId = ReqStatus.Succeeded;
|
||||
})
|
||||
.addCase(fetchNotesByDocumentId.rejected, (state, action) => {
|
||||
state.status.fetchNotesByDocumentId = ReqStatus.Failed;
|
||||
state.error.fetchNotesByDocumentId = action.payload ?? action.error;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const notesReducer = notesSlice.reducer;
|
||||
|
||||
export const {
|
||||
selectAll: selectAllNotes,
|
||||
selectById: selectNoteById,
|
||||
selectIds: selectNoteIds,
|
||||
} = notesAdapter.getSelectors((state: State) => state.notes);
|
Loading…
Add table
Add a link
Reference in a new issue