Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solutions][Cases] Cases Redesign #73247

Merged
merged 43 commits into from
Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a8f10f9
Create UserActionTimestamp component
cnasikas Jul 26, 2020
e7657d8
Improve UserActionAvatar component
cnasikas Jul 26, 2020
a76de88
Init layout
cnasikas Jul 26, 2020
1842c09
Add actions
cnasikas Jul 26, 2020
e2eee10
Add footers
cnasikas Jul 26, 2020
226feb4
Add username tooltip
cnasikas Jul 27, 2020
39df02d
Add loading spinner to avatar
cnasikas Jul 27, 2020
e7c013c
Move to comment
cnasikas Jul 27, 2020
a5aad3c
Add bottom actions
cnasikas Jul 27, 2020
20fccf7
Fix timestamp updated text
cnasikas Jul 27, 2020
433305f
Remove unused components
cnasikas Jul 27, 2020
5191e24
Fix unnecessary render
cnasikas Jul 27, 2020
e29e17a
Add username with avatar to events
cnasikas Jul 27, 2020
e1d4f0d
Init new markdown
cnasikas Sep 3, 2020
3217118
Improve markdown layout
cnasikas Sep 3, 2020
7f74dbb
Create timeline plugin
cnasikas Sep 4, 2020
f49a992
Fix unit tests
cnasikas Sep 4, 2020
0472664
Fix types
cnasikas Sep 7, 2020
3424f73
Add more tests
cnasikas Sep 8, 2020
f53bed4
Support timeline's old formatting
cnasikas Sep 10, 2020
0317e92
Rename markdown form
cnasikas Sep 10, 2020
b7646a2
Move useTimelineClick to common utils
cnasikas Sep 10, 2020
6217682
Improve tags
cnasikas Sep 10, 2020
aca89d2
Improve UserActionAvatar logic
cnasikas Sep 10, 2020
5d2f6d8
Use formatUrl
cnasikas Sep 10, 2020
0aa24bd
Improve callbacks
cnasikas Sep 10, 2020
84045d4
Improve complexity
cnasikas Sep 10, 2020
29b1892
Parse only timeline's urls
cnasikas Sep 14, 2020
432777d
Fix cypress tests
cnasikas Sep 14, 2020
5952cac
Fix copy reference link
cnasikas Sep 14, 2020
33f9397
Improve memoization
cnasikas Sep 14, 2020
488d898
Unskip tests
cnasikas Sep 14, 2020
7273287
Create UserActionContentToolbar
cnasikas Sep 14, 2020
54ed34b
Improve id
cnasikas Sep 14, 2020
aafbc26
Revert to old formatting
cnasikas Sep 16, 2020
a015a72
use useFormatUrl + simplify parser
XavierM Sep 16, 2020
6bf8009
Update rule note to use new markdown component
yctercero Sep 16, 2020
7185d51
Add translations
cnasikas Sep 16, 2020
b2f5d9d
Improve variable
cnasikas Sep 16, 2020
b544b70
Remove old markdown
cnasikas Sep 16, 2020
4c21106
Merge branch 'master' into cases_redesign
elasticmachine Sep 16, 2020
421548b
Fix cypress tests
cnasikas Sep 17, 2020
aa50719
Merge branch 'master' into cases_redesign
elasticmachine Sep 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
addNewCase,
selectCase,
} from '../tasks/timeline';
import { DESCRIPTION_INPUT } from '../screens/create_new_case';
import { DESCRIPTION_INPUT, ADD_COMMENT_INPUT } from '../screens/create_new_case';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { caseTimeline, TIMELINE_CASE_ID } from '../objects/case';

Expand Down Expand Up @@ -66,7 +66,7 @@ describe('attach timeline to case', () => {
selectCase(TIMELINE_CASE_ID);

cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
cy.get(ADD_COMMENT_INPUT).should(
'have.text',
`[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

export const ADD_COMMENT_INPUT = '[data-test-subj="add-comment"] textarea';

export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]';

export const DESCRIPTION_INPUT = '[data-test-subj="caseDescription"] textarea';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const onChangeEditable = jest.fn();
const onSaveContent = jest.fn();

const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c';
const timelineMarkdown = `!{timeline{"id":"${timelineId}","title":"timeline"}}`;
const timelineMarkdown = `[timeline](http://localhost:5601/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t))`;
const defaultProps = {
content: `A link to a timeline ${timelineMarkdown}`,
id: 'markdown-id',
Expand Down Expand Up @@ -49,7 +49,7 @@ describe('UserActionMarkdown ', () => {

expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
graphEventId: undefined,
graphEventId: '',
timelineId,
updateIsLoading: expect.any(Function),
updateTimeline: expect.any(Function),
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('UserActionMarkdown ', () => {
.simulate('click');
expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
graphEventId: undefined,
graphEventId: '',
timelineId,
updateIsLoading: expect.any(Function),
updateTimeline: expect.any(Function),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import { useGetUrlSearch } from '../navigation/use_get_url_search';
import { navTabs } from '../../../app/home/home_navigations';
import { APP_ID } from '../../../../common/constants';
import { useKibana } from '../../lib/kibana';
import { getTimelineUrl } from './redirect_to_timelines';

export { getDetectionEngineUrl } from './redirect_to_detection_engine';
export { getAppOverviewUrl } from './redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts';
export { getNetworkUrl, getNetworkDetailsUrl } from './redirect_to_network';
export { getTimelinesUrl, getTimelineTabsUrl } from './redirect_to_timelines';
export { getTimelinesUrl, getTimelineTabsUrl, getTimelineUrl } from './redirect_to_timelines';
export {
getCaseDetailsUrl,
getCaseUrl,
Expand All @@ -43,3 +44,18 @@ export const useFormatUrl = (page: SecurityPageName) => {
);
return { formatUrl, search };
};

export const useTimelineUrl = () => {
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
const { getUrlForApp } = useKibana().services.application;
const baseUrl = getUrlForApp(`${APP_ID}:${SecurityPageName.timelines}`, {
path: '',
absolute: true,
});

const timelineUrl = useCallback(
(id: string, graphEventId?: string) => `${baseUrl}${getTimelineUrl(id ?? '', graphEventId)}`,
[baseUrl]
);

return { timelineUrl };
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { isEmpty } from 'lodash/fp';
import { TimelineTypeLiteral } from '../../../../common/types/timeline';
import { appendSearch } from './helpers';

export const getTimelinesUrl = (search?: string) => `${appendSearch(search)}`;

export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) =>
`/${tabName}${appendSearch(search)}`;

export const getTimelineUrl = (id: string, graphEventId?: string) =>
`?timeline=(id:'${id}',isOpen:!t${
isEmpty(graphEventId) ? ')' : `,graphEventId:'${graphEventId}')`
}`;
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@
*/

export const ID = 'timeline';
export const PREFIX = `!{${ID}`;
export const OLD_PREFIX = `[`;
export const PREFIX = `[`;
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { isEmpty } from 'lodash';
import { Plugin } from '@elastic/eui/node_modules/unified';
import { RemarkTokenizer } from '@elastic/eui';
import { ID, PREFIX, OLD_PREFIX } from './constants';
import { TimelineConfiguration } from './types';

const START_POS = PREFIX.length;

const requiredFields = ['id', 'title'];

const validateConfiguration = (configuration: TimelineConfiguration) => {
requiredFields.forEach((field) => {
if (isEmpty(configuration[field])) {
throw new Error(`${field} is missing.`);
}
});
};
import { ID, PREFIX } from './constants';

export const TimelineParser: Plugin = function () {
const Parser = this.Parser;
const tokenizers = Parser.prototype.inlineTokenizers;
const methods = Parser.prototype.inlineMethods;
let oldFormat = false;

const parseOldFormat: RemarkTokenizer = function (eat, value, silent) {
const parseTimeline: RemarkTokenizer = function (eat, value, silent) {
let index = 0;
const nextChar = value[index];

Expand Down Expand Up @@ -110,81 +95,19 @@ export const TimelineParser: Plugin = function () {
});
};

const parseNewFormat: RemarkTokenizer = function (eat, value, silent) {
const nextChar = value[START_POS];

if (nextChar !== '{') {
return false;
}

if (silent) {
return true;
}

const hasConfiguration = nextChar === '{';

let match = PREFIX;
let configuration = { id: null, title: '', url: '' };

if (hasConfiguration) {
let configurationString = '';

let openObjects = 0;

for (let i = START_POS; i < value.length; i++) {
const char = value[i];
if (char === '{') {
openObjects++;
configurationString += char;
} else if (char === '}') {
openObjects--;
if (openObjects === -1) {
break;
}
configurationString += char;
} else {
configurationString += char;
}
}

match += configurationString;
try {
configuration = JSON.parse(configurationString);
validateConfiguration(configuration);
} catch (e) {
const now = eat.now();
this.file.fail(`Timeline parsing error: ${e}`, {
line: now.line,
column: now.column + 7,
});
}
}

match += '}';
return eat(match)({
type: ID,
...configuration,
});
};

const tokenizeTimeline: RemarkTokenizer = function tokenizeTimeline(eat, value, silent) {
if (
(value.startsWith(PREFIX) === false && value.startsWith(OLD_PREFIX) === false) ||
(value.startsWith(OLD_PREFIX) === true && !value.includes('timelines?timeline=(id:'))
value.startsWith(PREFIX) === false ||
(value.startsWith(PREFIX) === true && !value.includes('timelines?timeline=(id:'))
) {
return false;
}

if (value.startsWith(OLD_PREFIX)) {
oldFormat = true;
return parseOldFormat.call(this, eat, value, silent);
}

return parseNewFormat.call(this, eat, value, silent);
return parseTimeline.call(this, eat, value, silent);
};

tokenizeTimeline.locator = (value: string, fromIndex: number) => {
return value.indexOf(oldFormat ? OLD_PREFIX : PREFIX, fromIndex);
return value.indexOf(PREFIX, fromIndex);
};

tokenizers.timeline = tokenizeTimeline;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
import { TimelineType } from '../../../../../../common/types/timeline';
import { SelectableTimeline } from '../../../../../timelines/components/timeline/selectable_timeline';
import { OpenTimelineResult } from '../../../../../timelines/components/open_timeline/types';
import { useTimelineUrl } from '../../../link_to';

import { ID, PREFIX } from './constants';
import { ID } from './constants';
import * as i18n from './translations';

interface TimelineEditorProps {
Expand All @@ -25,6 +26,8 @@ interface TimelineEditorProps {
}

const TimelineEditorComponent: React.FC<TimelineEditorProps> = ({ onClosePopover, onInsert }) => {
const { timelineUrl } = useTimelineUrl();

const handleGetSelectableOptions = useCallback(
({ timelines }: { timelines: OpenTimelineResult[] }) => [
...timelines.map(
Expand All @@ -49,16 +52,10 @@ const TimelineEditorComponent: React.FC<TimelineEditorProps> = ({ onClosePopover
hideUntitled={true}
getSelectableOptions={handleGetSelectableOptions}
onTimelineChange={(timelineTitle, timelineId, graphEventId) => {
onInsert(
`${PREFIX}${JSON.stringify({
id: timelineId,
title: timelineTitle,
graphEventId,
})}}`,
{
block: false,
}
);
const url = timelineUrl(timelineId ?? '', graphEventId);
onInsert(`[${timelineTitle}](${url})`, {
block: false,
});
}}
onClosePopover={onClosePopover}
timelineType={TimelineType.default}
Expand All @@ -76,8 +73,8 @@ export const plugin: EuiMarkdownEditorUiPlugin = {
iconType: 'timeline',
},
helpText: (
<EuiCodeBlock language="js" paddingSize="s" fontSize="l">
{`${PREFIX}{"id":"Timeline id","title":"Timeline title"}}`}
<EuiCodeBlock language="md" paddingSize="s" fontSize="l">
{'[title](url)'}
</EuiCodeBlock>
),
editor: function editor({ node, onSave, onCancel }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { isEmpty } from 'lodash/fp';
import { useCallback, useState, useEffect } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { useBasePath } from '../../../../common/lib/kibana';
import { useTimelineUrl } from '../../../../common/components/link_to';
import { CursorPosition } from '../../../../common/components/markdown_editor';
import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline';
import { setInsertTimeline } from '../../../store/timeline/actions';

export const useInsertTimeline = (value: string, onChange: (newValue: string) => void) => {
const basePath = window.location.origin + useBasePath();
const dispatch = useDispatch();
const { timelineUrl } = useTimelineUrl();
const [cursorPosition, setCursorPosition] = useState<CursorPosition>({
start: 0,
end: 0,
Expand All @@ -24,21 +23,19 @@ export const useInsertTimeline = (value: string, onChange: (newValue: string) =>

const handleOnTimelineChange = useCallback(
(title: string, id: string | null, graphEventId?: string) => {
const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}'${
!isEmpty(graphEventId) ? `,graphEventId:'${graphEventId}'` : ''
},isOpen:!t)`;
const url = timelineUrl(id ?? '', graphEventId);

const newValue: string = [
value.slice(0, cursorPosition.start),
cursorPosition.start === cursorPosition.end
? `[${title}](${builtLink})`
: `[${value.slice(cursorPosition.start, cursorPosition.end)}](${builtLink})`,
? `[${title}](${url})`
: `[${value.slice(cursorPosition.start, cursorPosition.end)}](${url})`,
value.slice(cursorPosition.end),
].join('');

onChange(newValue);
},
[value, onChange, basePath, cursorPosition]
[value, onChange, cursorPosition, timelineUrl]
);

const handleCursorChange = useCallback((cp: CursorPosition) => {
Expand Down