From 8fdb146e8776468f81f5dca204551d9e3109c1d9 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 9 Jan 2020 13:47:29 -0700 Subject: [PATCH 01/67] initial case ui in siem --- x-pack/legacy/plugins/siem/index.ts | 12 + .../siem/public/components/link_to/index.ts | 1 + .../public/components/link_to/link_to.tsx | 2 + .../components/link_to/redirect_to_case.tsx | 20 + .../public/components/url_state/constants.ts | 8 +- .../siem/public/components/url_state/types.ts | 1 + .../public/containers/case/index.gql_query.ts | 34 ++ .../siem/public/containers/case/index.tsx | 76 +++ .../siem/public/graphql/introspection.json | 480 +++++++++++++++++- .../plugins/siem/public/graphql/types.ts | 145 +++++- .../siem/public/pages/case/case.test.tsx | 70 +++ .../plugins/siem/public/pages/case/case.tsx | 77 +++ .../plugins/siem/public/pages/case/index.tsx | 13 + .../siem/public/pages/case/translations.ts | 36 ++ .../public/pages/home/home_navigations.tsx | 8 + .../plugins/siem/public/pages/home/index.tsx | 2 + .../siem/public/pages/home/translations.ts | 4 + .../plugins/siem/public/pages/home/types.ts | 4 +- .../plugins/siem/server/graphql/case/index.ts | 8 + .../siem/server/graphql/case/resolvers.ts | 55 ++ .../siem/server/graphql/case/schema.gql.ts | 76 +++ .../plugins/siem/server/graphql/index.ts | 2 + .../plugins/siem/server/graphql/types.ts | 263 +++++++++- .../legacy/plugins/siem/server/init_server.ts | 2 + .../siem/server/lib/case/saved_object.ts | 112 ++++ ...pings_temp.ts => saved_object_mappings.ts} | 0 .../plugins/siem/server/lib/compose/kibana.ts | 3 + .../legacy/plugins/siem/server/lib/types.ts | 2 + .../plugins/siem/server/saved_objects.ts | 6 + .../api_integration/apis/siem/ip_overview.ts | 2 +- 30 files changed, 1496 insertions(+), 28 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/index.gql_query.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/case.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/translations.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/case/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts rename x-pack/legacy/plugins/siem/server/lib/case/{saved_object_mappings_temp.ts => saved_object_mappings.ts} (100%) diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index c5038626fdfc20..0bbf3b0851be15 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -135,6 +135,18 @@ export const siem = (kibana: any) => { }, }, mappings: savedObjectMappings, + + // TODO: Remove once while Saved Object Mappings API is programmed for the NP See: https://github.com/elastic/kibana/issues/50309 + savedObjectSchemas: { + 'case-workflow': { + indexPattern: '.case-testing-ground', // TODO: Change this name and use kibana.yml settings to override it. + isNamespaceAgnostic: false, + }, + 'case-workflow-comment': { + indexPattern: '.case-testing-ground', // TODO: Change this name and use kibana.yml settings to override it. + isNamespaceAgnostic: false, + }, + }, }, init(server: Server) { const { config, newPlatform, plugins, route } = server; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index 10198345755c37..2f8d8e929b4c23 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -13,3 +13,4 @@ export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; export { getHostsUrl, getHostDetailsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines'; +export { getCaseUrl, RedirectToCasePage } from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 5a7f6ef1274c91..8a3e9f3a60a642 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,6 +20,7 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; +import { RedirectToCasePage } from './redirect_to_case'; interface LinkToPageProps { match: RouteMatch<{}>; @@ -31,6 +32,7 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToOverviewPage} path={`${match.url}/:pageName(${SiemPageName.overview})`} /> + ; + +export const RedirectToCasePage = ({ location: { search } }: CaseComponentProps) => ( + +); + +export const getCaseUrl = () => `#/link-to/${SiemPageName.case}`; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts index 2e700e3e23b64d..6468d1299832bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts @@ -20,4 +20,10 @@ export enum CONSTANTS { unknown = 'unknown', } -export type UrlStateType = 'detection-engine' | 'host' | 'network' | 'overview' | 'timeline'; +export type UrlStateType = + | 'detection-engine' + | 'host' + | 'network' + | 'overview' + | 'timeline' + | 'case'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index 4eb6398cc7773d..3b130f4a7a53f2 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -41,6 +41,7 @@ export const URL_STATE_KEYS: Record = { ], overview: [CONSTANTS.timeline, CONSTANTS.timerange], timeline: [CONSTANTS.timeline, CONSTANTS.timerange], + case: [], }; export type LocationTypes = diff --git a/x-pack/legacy/plugins/siem/public/containers/case/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/case/index.gql_query.ts new file mode 100644 index 00000000000000..875853eb413029 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/index.gql_query.ts @@ -0,0 +1,34 @@ +/* + * 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 gql from 'graphql-tag'; + +export const caseQuery = gql` + query GetCaseQuery($caseId: ID!) { + getCase(caseId: $caseId) { + id + type + updated_at + version + attributes { + assignees { + username + full_name + } + case_type + created_at + created_by { + username + full_name + } + description + state + tags + title + } + } + } +`; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/index.tsx new file mode 100644 index 00000000000000..47dfd1b97c946f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/index.tsx @@ -0,0 +1,76 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { GetCaseQuery, CaseSavedObject } from '../../graphql/types'; +import { inputsModel } from '../../store'; +import { getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { caseQuery } from './index.gql_query'; + +const ID = 'caseQuery'; + +export interface CaseArgs { + id: string; + caseData: CaseSavedObject; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface CaseProps extends QueryTemplateProps { + caseId: string; + children: (args: CaseArgs) => React.ReactNode; +} + +const CaseComponentQuery = React.memo( + ({ id = ID, children, skip, sourceId, caseId }) => ( + + query={caseQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + caseId, + }} + > + {({ data, loading, refetch }) => { + const init: CaseSavedObject = { + id: caseId, + type: '', + updated_at: '', + version: '', + attributes: { + assignees: [], + case_type: '', + created_at: 1234235345, + created_by: { + username: '', + full_name: null, + }, + description: '', + state: 'open', + tags: [], + title: '', + }, + }; + const caseData: CaseSavedObject = getOr(init, 'getCase', data); + return children({ + id, + caseData, + loading, + refetch, + }); + }} + + ) +); + +CaseComponentQuery.displayName = 'CaseComponentQuery'; + +export const CaseQuery = CaseComponentQuery; diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 8ebc66b7f38a70..2ab5530db5005c 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -9,6 +9,60 @@ "name": "Query", "description": "", "fields": [ + { + "name": "getCase", + "description": "", + "args": [ + { + "name": "caseId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "CaseSavedObject", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "getAllCases", + "description": "", + "args": [ + { + "name": "pageInfo", + "description": "", + "type": { "kind": "INPUT_OBJECT", "name": "PageInfoCase", "ofType": null }, + "defaultValue": null + }, + { + "name": "search", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "sort", + "description": "", + "type": { "kind": "INPUT_OBJECT", "name": "SortCase", "ofType": null }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "ResponseCases", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "getNote", "description": "", @@ -277,35 +331,47 @@ }, { "kind": "OBJECT", - "name": "NoteResult", + "name": "CaseSavedObject", "description": "", "fields": [ { - "name": "eventId", + "name": "attributes", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "CaseResult", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "note", + "name": "id", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "timelineId", + "name": "type", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "noteId", + "name": "updated_at", "description": "", "args": [], "type": { @@ -317,47 +383,157 @@ "deprecationReason": null }, { - "name": "created", + "name": "version", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CaseResult", + "description": "", + "fields": [ + { + "name": "assignees", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "ElasticUser", "ofType": null } + } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "createdBy", + "name": "case_type", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "timelineVersion", + "name": "created_at", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "updated", + "name": "created_by", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "ElasticUser", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "updatedBy", + "name": "description", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "version", + "name": "state", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tags", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ElasticUser", + "description": "", + "fields": [ + { + "name": "username", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "full_name", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -392,7 +568,7 @@ }, { "kind": "INPUT_OBJECT", - "name": "PageInfoNote", + "name": "PageInfoCase", "description": "", "fields": null, "inputFields": [ @@ -423,7 +599,7 @@ }, { "kind": "INPUT_OBJECT", - "name": "SortNote", + "name": "SortCase", "description": "", "fields": null, "inputFields": [ @@ -433,7 +609,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "ENUM", "name": "SortFieldNote", "ofType": null } + "ofType": { "kind": "ENUM", "name": "SortFieldCase", "ofType": null } }, "defaultValue": null }, @@ -454,7 +630,7 @@ }, { "kind": "ENUM", - "name": "SortFieldNote", + "name": "SortFieldCase", "description": "", "fields": null, "inputFields": null, @@ -483,6 +659,220 @@ ], "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "ResponseCases", + "description": "", + "fields": [ + { + "name": "cases", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "CaseResult", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NoteResult", + "description": "", + "fields": [ + { + "name": "eventId", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "note", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timelineId", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "noteId", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "created", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdBy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timelineVersion", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedBy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PageInfoNote", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "pageIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "pageSize", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SortNote", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "sortField", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "SortFieldNote", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "sortOrder", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "SortFieldNote", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "updatedBy", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updated", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "ResponseNotes", @@ -10265,6 +10655,33 @@ "name": "Mutation", "description": "", "fields": [ + { + "name": "deleteCase", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } + } + } + }, + "defaultValue": null + } + ], + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "persistNote", "description": "Persists a note", @@ -11917,6 +12334,23 @@ ], "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CaseInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "caseId", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "EcsEdges", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 6dfde08058f7cb..65ae8240f6d44d 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -8,6 +8,18 @@ export type Maybe = T | null; +export interface PageInfoCase { + pageIndex: number; + + pageSize: number; +} + +export interface SortCase { + sortField: SortFieldCase; + + sortOrder: Direction; +} + export interface PageInfoNote { pageIndex: number; @@ -257,6 +269,10 @@ export interface SortTimelineInput { sortDirection?: Maybe; } +export interface CaseInput { + caseId?: Maybe; +} + export interface FavoriteTimelineInput { fullName?: Maybe; @@ -265,7 +281,7 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldNote { +export enum SortFieldCase { updatedBy = 'updatedBy', updated = 'updated', } @@ -275,6 +291,11 @@ export enum Direction { desc = 'desc', } +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', +} + export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -378,6 +399,10 @@ export type EsValue = any; // ==================================================== export interface Query { + getCase: CaseSavedObject; + + getAllCases: ResponseCases; + getNote: NoteResult; getNotesByTimelineId: NoteResult[]; @@ -397,6 +422,48 @@ export interface Query { getAllTimeline: ResponseTimelines; } +export interface CaseSavedObject { + attributes: CaseResult; + + id: string; + + type: string; + + updated_at: string; + + version: string; +} + +export interface CaseResult { + assignees: (Maybe)[]; + + case_type: string; + + created_at: number; + + created_by: ElasticUser; + + description: string; + + state: string; + + tags: (Maybe)[]; + + title: string; +} + +export interface ElasticUser { + username: string; + + full_name?: Maybe; +} + +export interface ResponseCases { + cases: CaseResult[]; + + totalCount?: Maybe; +} + export interface NoteResult { eventId?: Maybe; @@ -2014,6 +2081,7 @@ export interface ResponseTimelines { } export interface Mutation { + deleteCase?: Maybe; /** Persists a note */ persistNote: ResponseNote; @@ -2112,6 +2180,16 @@ export interface HostFields { // Arguments // ==================================================== +export interface GetCaseQueryArgs { + caseId: string; +} +export interface GetAllCasesQueryArgs { + pageInfo?: Maybe; + + search?: Maybe; + + sort?: Maybe; +} export interface GetNoteQueryArgs { id: string; } @@ -2409,6 +2487,9 @@ export interface IndicesExistSourceStatusArgs { export interface IndexFieldsSourceStatusArgs { defaultIndex: string[]; } +export interface DeleteCaseMutationArgs { + id: string[]; +} export interface PersistNoteMutationArgs { noteId?: Maybe; @@ -2750,6 +2831,68 @@ export namespace GetAuthenticationsQuery { }; } +export namespace GetCaseQuery { + export type Variables = { + caseId: string; + }; + + export type Query = { + __typename?: 'Query'; + + getCase: GetCase; + }; + + export type GetCase = { + __typename?: 'CaseSavedObject'; + + id: string; + + type: string; + + updated_at: string; + + version: string; + + attributes: Attributes; + }; + + export type Attributes = { + __typename?: 'CaseResult'; + + assignees: (Maybe)[]; + + case_type: string; + + created_at: number; + + created_by: CreatedBy; + + description: string; + + state: string; + + tags: (Maybe)[]; + + title: string; + }; + + export type Assignees = { + __typename?: 'ElasticUser'; + + username: string; + + full_name: Maybe; + }; + + export type CreatedBy = { + __typename?: 'ElasticUser'; + + username: string; + + full_name: Maybe; + }; +} + export namespace GetEventsOverTimeQuery { export type Variables = { sourceId: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx new file mode 100644 index 00000000000000..61ef36a58fc2df --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { MemoryRouter } from 'react-router-dom'; + +import { TestProviders } from '../../mock'; +import { mocksSource } from '../../containers/source/mock'; +import { Case } from './index'; + +let localSource: Array<{ + request: {}; + result: { + data: { + source: { + status: { + indicesExist: boolean; + }; + }; + }; + }; +}>; + +describe('Case', () => { + describe('rendering', () => { + beforeEach(() => { + localSource = cloneDeep(mocksSource); + }); + + test('it renders the Setup Instructions text when no index is available', async () => { + localSource[0].result.data.source.status.indicesExist = false; + const wrapper = mount( + + + + + + + + ); + // Why => https://github.com/apollographql/react-apollo/issues/1711 + await new Promise(resolve => setTimeout(resolve)); + wrapper.update(); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + }); + + test('it DOES NOT render the Getting started text when an index is available', async () => { + localSource[0].result.data.source.status.indicesExist = true; + const wrapper = mount( + + + + + + + + ); + // Why => https://github.com/apollographql/react-apollo/issues/1711 + await new Promise(resolve => setTimeout(resolve)); + wrapper.update(); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx new file mode 100644 index 00000000000000..5bb4cd26240c17 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -0,0 +1,77 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import React from 'react'; +import chrome from 'ui/chrome'; + +import { useKibana } from '../../lib/kibana'; +import { EmptyPage } from '../../components/empty_page'; +import { HeaderPage } from '../../components/header_page'; +import { WrapperPage } from '../../components/wrapper_page'; +import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { CaseQuery } from '../../containers/case'; +import * as i18n from './translations'; + +const basePath = chrome.getBasePath(); + +export const CaseComponent = React.memo(() => { + const docLinks = useKibana().services.docLinks; + + return ( + <> + + + + + {({ indicesExist }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + {children => ( +
+

{`Case: ${children.caseData.attributes.title}`}

+
    +
  • {`Description: ${children.caseData.attributes.description}`}
  • +
  • {`Type: ${children.caseData.attributes.case_type}`}
  • +
  • {`State: ${children.caseData.attributes.state}`}
  • +
+
+ )} +
+
+ ) : ( + + ) + } +
+
+ + + + ); +}); +CaseComponent.displayName = 'CaseComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx new file mode 100644 index 00000000000000..552f16c1060a7b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -0,0 +1,13 @@ +/* + * 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, { memo } from 'react'; + +import { CaseComponent } from './case'; + +export const Case = memo(() => ); + +Case.displayName = 'Case'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts new file mode 100644 index 00000000000000..277433518f084d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -0,0 +1,36 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { + defaultMessage: 'Case Workflows', +}); + +export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.pageSubtitle', { + defaultMessage: 'Case Workflow Management within the Elastic SIEM', +}); + +export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.pageBadgeLabel', { + defaultMessage: 'Beta', +}); + +export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.pageBadgeTooltip', { + defaultMessage: + 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', +}); + +export const EMPTY_TITLE = i18n.translate('xpack.siem.case.emptyTitle', { + defaultMessage: 'It looks like you don’t have any indices relevant to the SIEM application', +}); + +export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.case.emptyActionPrimary', { + defaultMessage: 'View setup instructions', +}); + +export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.case.emptyActionSecondary', { + defaultMessage: 'Go to documentation', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index 220f8a958aa43b..f033d7ab87bbbf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -10,6 +10,7 @@ import { getNetworkUrl, getTimelinesUrl, getHostsUrl, + getCaseUrl, } from '../../components/link_to'; import * as i18n from './translations'; import { SiemPageName, SiemNavTab } from './types'; @@ -50,4 +51,11 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'timeline', }, + [SiemPageName.case]: { + id: SiemPageName.case, + name: i18n.CASE, + href: getCaseUrl(), + disabled: false, + urlKey: 'case', + }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index a545be447796dc..f95436d52abfa0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -26,6 +26,7 @@ import { DetectionEngineContainer } from '../detection_engine'; import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; +import { Case } from '../case'; import { Timelines } from '../timelines'; import { navTabs } from './home_navigations'; import { SiemPageName } from './types'; @@ -127,6 +128,7 @@ export const HomePage: React.FC = () => ( )} /> + } /> } /> diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts index b87ea1c17a117e..cbd2600d68197c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts @@ -25,3 +25,7 @@ export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionE export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { defaultMessage: 'Timelines', }); + +export const CASE = i18n.translate('xpack.siem.navigation.case', { + defaultMessage: 'Case', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/legacy/plugins/siem/public/pages/home/types.ts index 101c6a69b08d19..b31f3b5d572881 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/types.ts @@ -12,6 +12,7 @@ export enum SiemPageName { network = 'network', detectionEngine = 'detection-engine', timelines = 'timelines', + case = 'case', } export type SiemNavTabKey = @@ -19,6 +20,7 @@ export type SiemNavTabKey = | SiemPageName.hosts | SiemPageName.network | SiemPageName.detectionEngine - | SiemPageName.timelines; + | SiemPageName.timelines + | SiemPageName.case; export type SiemNavTab = Record; diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/index.ts b/x-pack/legacy/plugins/siem/server/graphql/case/index.ts new file mode 100644 index 00000000000000..77c2c5db7159f8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/case/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { createCaseResolvers } from './resolvers'; +export { caseSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts new file mode 100644 index 00000000000000..eaa4e6e9c81ac9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts @@ -0,0 +1,55 @@ +/* + * 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 { AppResolverWithFields, AppResolverOf } from '../../lib/framework'; +import { MutationResolvers, QueryResolvers } from '../types'; +import { Case } from '../../lib/case/saved_object'; + +export type QueryCaseResolver = AppResolverOf; + +export type QueryAllCaseResolver = AppResolverWithFields< + QueryResolvers.GetAllCasesResolver, + 'totalCount' | 'Case' +>; + +export type MutationDeleteCaseResolver = AppResolverOf; + +interface CaseResolversDeps { + case: Case; +} + +export const createCaseResolvers = ( + libs: CaseResolversDeps +): { + Query: { + getCase: QueryCaseResolver; + getAllCases: QueryAllCaseResolver; + }; + Mutation: { + deleteCase: MutationDeleteCaseResolver; + }; +} => ({ + Query: { + async getCase(root, args, { req }) { + return libs.case.getCase(req, args.caseId); + }, + async getAllCases(root, args, { req }) { + return libs.case.getAllCases( + req, + args.pageInfo || null, + args.search || null, + args.sort || null + ); + }, + }, + Mutation: { + async deleteCase(root, args, { req }) { + await libs.case.deleteCase(req, args.id); + + return true; + }, + }, +}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts new file mode 100644 index 00000000000000..28a4898f33e731 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts @@ -0,0 +1,76 @@ +/* + * 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 gql from 'graphql-tag'; + +export const caseSchema = gql` + ############### + #### INPUT #### + ############### + + input CaseInput { + caseId: String + } + + input PageInfoCase { + pageIndex: Float! + pageSize: Float! + } + + enum SortFieldCase { + updatedBy + updated + } + + input SortCase { + sortField: SortFieldCase! + sortOrder: Direction! + } + + ############### + #### QUERY #### + ############### + type ElasticUser { + username: String! + full_name: String + } + type CaseResult { + assignees: [ElasticUser]! + case_type: String! + created_at: Float! + created_by: ElasticUser! + description: String! + state: String! + tags: [String]! + title: String! + } + + type CaseSavedObject { + attributes: CaseResult! + id: String! + type: String! + updated_at: String! + version: String! + } + + type ResponseCases { + cases: [CaseResult!]! + totalCount: Float + } + + ######################### + #### Mutation/Query #### + ######################### + + extend type Query { + getCase(caseId: ID!): CaseSavedObject! + getAllCases(pageInfo: PageInfoCase, search: String, sort: SortCase): ResponseCases! + } + + extend type Mutation { + deleteCase(id: [ID!]!): Boolean + } +`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 762b9002a466da..e0a50235a3ad02 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -9,6 +9,7 @@ import { sharedSchema } from '../../common/graphql/shared'; import { anomaliesSchema } from './anomalies'; import { authenticationsSchema } from './authentications'; +import { caseSchema } from './case'; import { ecsSchema } from './ecs'; import { eventsSchema } from './events'; import { hostsSchema } from './hosts'; @@ -34,6 +35,7 @@ export const schemas = [ alertsSchema, anomaliesSchema, authenticationsSchema, + caseSchema, ecsSchema, eventsSchema, dateSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index 776444b1502b19..90c174fdd51c82 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -10,6 +10,18 @@ import { SiemContext } from '../lib/types'; export type Maybe = T | null; +export interface PageInfoCase { + pageIndex: number; + + pageSize: number; +} + +export interface SortCase { + sortField: SortFieldCase; + + sortOrder: Direction; +} + export interface PageInfoNote { pageIndex: number; @@ -259,6 +271,10 @@ export interface SortTimelineInput { sortDirection?: Maybe; } +export interface CaseInput { + caseId?: Maybe; +} + export interface FavoriteTimelineInput { fullName?: Maybe; @@ -267,7 +283,7 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldNote { +export enum SortFieldCase { updatedBy = 'updatedBy', updated = 'updated', } @@ -277,6 +293,11 @@ export enum Direction { desc = 'desc', } +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', +} + export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -380,6 +401,10 @@ export type EsValue = any; // ==================================================== export interface Query { + getCase: CaseSavedObject; + + getAllCases: ResponseCases; + getNote: NoteResult; getNotesByTimelineId: NoteResult[]; @@ -399,6 +424,48 @@ export interface Query { getAllTimeline: ResponseTimelines; } +export interface CaseSavedObject { + attributes: CaseResult; + + id: string; + + type: string; + + updated_at: string; + + version: string; +} + +export interface CaseResult { + assignees: (Maybe)[]; + + case_type: string; + + created_at: number; + + created_by: ElasticUser; + + description: string; + + state: string; + + tags: (Maybe)[]; + + title: string; +} + +export interface ElasticUser { + username: string; + + full_name?: Maybe; +} + +export interface ResponseCases { + cases: CaseResult[]; + + totalCount?: Maybe; +} + export interface NoteResult { eventId?: Maybe; @@ -2016,6 +2083,7 @@ export interface ResponseTimelines { } export interface Mutation { + deleteCase?: Maybe; /** Persists a note */ persistNote: ResponseNote; @@ -2114,6 +2182,16 @@ export interface HostFields { // Arguments // ==================================================== +export interface GetCaseQueryArgs { + caseId: string; +} +export interface GetAllCasesQueryArgs { + pageInfo?: Maybe; + + search?: Maybe; + + sort?: Maybe; +} export interface GetNoteQueryArgs { id: string; } @@ -2411,6 +2489,9 @@ export interface IndicesExistSourceStatusArgs { export interface IndexFieldsSourceStatusArgs { defaultIndex: string[]; } +export interface DeleteCaseMutationArgs { + id: string[]; +} export interface PersistNoteMutationArgs { noteId?: Maybe; @@ -2499,6 +2580,10 @@ export type DirectiveResolverFn = ( export namespace QueryResolvers { export interface Resolvers { + getCase?: GetCaseResolver; + + getAllCases?: GetAllCasesResolver; + getNote?: GetNoteResolver; getNotesByTimelineId?: GetNotesByTimelineIdResolver; @@ -2522,6 +2607,29 @@ export namespace QueryResolvers { getAllTimeline?: GetAllTimelineResolver; } + export type GetCaseResolver = Resolver< + R, + Parent, + TContext, + GetCaseArgs + >; + export interface GetCaseArgs { + caseId: string; + } + + export type GetAllCasesResolver< + R = ResponseCases, + Parent = {}, + TContext = SiemContext + > = Resolver; + export interface GetAllCasesArgs { + pageInfo?: Maybe; + + search?: Maybe; + + sort?: Maybe; + } + export type GetNoteResolver = Resolver< R, Parent, @@ -2613,6 +2721,145 @@ export namespace QueryResolvers { } } +export namespace CaseSavedObjectResolvers { + export interface Resolvers { + attributes?: AttributesResolver; + + id?: IdResolver; + + type?: TypeResolver; + + updated_at?: UpdatedAtResolver; + + version?: VersionResolver; + } + + export type AttributesResolver< + R = CaseResult, + Parent = CaseSavedObject, + TContext = SiemContext + > = Resolver; + export type IdResolver = Resolver< + R, + Parent, + TContext + >; + export type TypeResolver = Resolver< + R, + Parent, + TContext + >; + export type UpdatedAtResolver< + R = string, + Parent = CaseSavedObject, + TContext = SiemContext + > = Resolver; + export type VersionResolver< + R = string, + Parent = CaseSavedObject, + TContext = SiemContext + > = Resolver; +} + +export namespace CaseResultResolvers { + export interface Resolvers { + assignees?: AssigneesResolver<(Maybe)[], TypeParent, TContext>; + + case_type?: CaseTypeResolver; + + created_at?: CreatedAtResolver; + + created_by?: CreatedByResolver; + + description?: DescriptionResolver; + + state?: StateResolver; + + tags?: TagsResolver<(Maybe)[], TypeParent, TContext>; + + title?: TitleResolver; + } + + export type AssigneesResolver< + R = (Maybe)[], + Parent = CaseResult, + TContext = SiemContext + > = Resolver; + export type CaseTypeResolver = Resolver< + R, + Parent, + TContext + >; + export type CreatedAtResolver = Resolver< + R, + Parent, + TContext + >; + export type CreatedByResolver< + R = ElasticUser, + Parent = CaseResult, + TContext = SiemContext + > = Resolver; + export type DescriptionResolver< + R = string, + Parent = CaseResult, + TContext = SiemContext + > = Resolver; + export type StateResolver = Resolver< + R, + Parent, + TContext + >; + export type TagsResolver< + R = (Maybe)[], + Parent = CaseResult, + TContext = SiemContext + > = Resolver; + export type TitleResolver = Resolver< + R, + Parent, + TContext + >; +} + +export namespace ElasticUserResolvers { + export interface Resolvers { + username?: UsernameResolver; + + full_name?: FullNameResolver, TypeParent, TContext>; + } + + export type UsernameResolver = Resolver< + R, + Parent, + TContext + >; + export type FullNameResolver< + R = Maybe, + Parent = ElasticUser, + TContext = SiemContext + > = Resolver; +} + +export namespace ResponseCasesResolvers { + export interface Resolvers { + cases?: CasesResolver; + + totalCount?: TotalCountResolver, TypeParent, TContext>; + } + + export type CasesResolver< + R = CaseResult[], + Parent = ResponseCases, + TContext = SiemContext + > = Resolver; + export type TotalCountResolver< + R = Maybe, + Parent = ResponseCases, + TContext = SiemContext + > = Resolver; +} + export namespace NoteResultResolvers { export interface Resolvers { eventId?: EventIdResolver, TypeParent, TContext>; @@ -8332,6 +8579,7 @@ export namespace ResponseTimelinesResolvers { export namespace MutationResolvers { export interface Resolvers { + deleteCase?: DeleteCaseResolver, TypeParent, TContext>; /** Persists a note */ persistNote?: PersistNoteResolver; @@ -8364,6 +8612,15 @@ export namespace MutationResolvers { deleteTimeline?: DeleteTimelineResolver; } + export type DeleteCaseResolver< + R = Maybe, + Parent = {}, + TContext = SiemContext + > = Resolver; + export interface DeleteCaseArgs { + id: string[]; + } + export type PersistNoteResolver = Resolver< R, Parent, @@ -8761,6 +9018,10 @@ export interface EsValueScalarConfig extends GraphQLScalarTypeConfig = { Query?: QueryResolvers.Resolvers; + CaseSavedObject?: CaseSavedObjectResolvers.Resolvers; + CaseResult?: CaseResultResolvers.Resolvers; + ElasticUser?: ElasticUserResolvers.Resolvers; + ResponseCases?: ResponseCasesResolvers.Resolvers; NoteResult?: NoteResultResolvers.Resolvers; ResponseNotes?: ResponseNotesResolvers.Resolvers; PinnedEvent?: PinnedEventResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index 5ecbb51c6770dd..ca185f16e4e23e 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -16,6 +16,7 @@ import { createKpiHostsResolvers } from './graphql/kpi_hosts'; import { createKpiNetworkResolvers } from './graphql/kpi_network'; import { createNetworkResolvers } from './graphql/network'; import { createNoteResolvers } from './graphql/note'; +import { createCaseResolvers } from './graphql/case'; import { createPinnedEventResolvers } from './graphql/pinned_event'; import { createOverviewResolvers } from './graphql/overview'; import { createScalarDateResolvers } from './graphql/scalar_date'; @@ -37,6 +38,7 @@ export const initServer = (libs: AppBackendLibs) => { createAlertsResolvers(libs) as IResolvers, createAnomaliesResolvers(libs) as IResolvers, createAuthenticationsResolvers(libs) as IResolvers, + createCaseResolvers(libs) as IResolvers, createEsValueResolvers() as IResolvers, createEventsResolvers(libs) as IResolvers, createHostsResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts new file mode 100644 index 00000000000000..17e98e16c99575 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts @@ -0,0 +1,112 @@ +/* + * 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 { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; +import { PageInfoCase, CaseSavedObject, ResponseCases, SortCase } from '../../graphql/types'; +import { FrameworkRequest } from '../framework'; +import { caseSavedObjectType } from './saved_object_mappings'; +export class Case { + public async deleteCase(request: FrameworkRequest, noteIds: string[]) { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + noteIds.map(noteId => savedObjectsClient.delete(caseSavedObjectType, noteId)) + ); + } + + public async getCase(request: FrameworkRequest, caseId: string): Promise { + return this.getSavedCase(request, caseId); + } + + public async getAllCases( + request: FrameworkRequest, + pageInfo: PageInfoCase | null, + search: string | null, + sort: SortCase | null + ): Promise { + const options: SavedObjectsFindOptions = { + type: caseSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: ['note'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return this.getAllSavedCase(request, options); + } + + private async getSavedCase(request: FrameworkRequest, caseId: string) { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(caseSavedObjectType, caseId); + // console.log( + // 'SAVED CASE!!!', + // JSON.stringify({ + // caseSavedObjectType, + // caseId, + // savedObject, + // }) + // ); + + return convertSavedObjectToSavedCase(savedObject); + } + + private async getAllSavedCase(request: FrameworkRequest, options: SavedObjectsFindOptions) { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObjects = await savedObjectsClient.find(options); + + return { + totalCount: savedObjects.total, + cases: savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedCase(savedObject) + ), + }; + } +} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const convertSavedObjectToSavedCase = (savedObject: any): any => savedObject; + +// ( +// savedObject: unknown, +// timelineVersion?: string | undefined | null +// ): CaseSavedObject => +// pipe( +// CaseSavedObjectRuntimeType.decode(savedObject), +// map(savedCase => ({ +// noteId: savedCase.id, +// version: savedCase.version, +// timelineVersion, +// ...savedCase.attributes, +// })), +// fold(errors => { +// throw new Error(failure(errors).join('\n')); +// }, identity) +// ); + +// we have to use any here because the SavedObjectAttributes interface is like below +// export interface SavedObjectAttributes { +// [key: string]: SavedObjectAttributes | string | number | boolean | null; +// } +// then this interface does not allow types without index signature +// this is limiting us with our type for now so the easy way was to use any +// +// const pickSavedCase = ( +// noteId: string | null, +// savedCase: SavedCase, +// userInfo: AuthenticatedUser | null +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// ): any => { +// if (noteId == null) { +// savedCase.created = new Date().valueOf(); +// savedCase.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; +// savedCase.updated = new Date().valueOf(); +// savedCase.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; +// } else if (noteId != null) { +// savedCase.updated = new Date().valueOf(); +// savedCase.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; +// } +// return savedCase; +// }; diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts rename to x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index 30fdf7520a3ed1..db4a769c54cd9a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -30,6 +30,7 @@ import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes'; +import { Case } from '../case/saved_object'; import { Note } from '../note/saved_object'; import { PinnedEvent } from '../pinned_event/saved_object'; import { Timeline } from '../timeline/saved_object'; @@ -47,6 +48,7 @@ export function compose( const timeline = new Timeline(); const note = new Note(); const pinnedEvent = new PinnedEvent(); + const caseWorkflow = new Case(); const domainLibs: AppDomainLibs = { alerts: new Alerts(new ElasticsearchAlertsAdapter(framework)), @@ -72,6 +74,7 @@ export function compose( timeline, note, pinnedEvent, + case: caseWorkflow, }; return libs; diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 9034ab4e6af83d..fc50969add08d5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -7,6 +7,7 @@ export { ConfigType as Configuration } from '../../../../../plugins/siem/server'; import { Anomalies } from './anomalies'; import { Authentications } from './authentications'; +import { Case } from './case/saved_object'; import { Events } from './events'; import { FrameworkAdapter, FrameworkRequest } from './framework'; import { Hosts } from './hosts'; @@ -44,6 +45,7 @@ export interface AppDomainLibs { } export interface AppBackendLibs extends AppDomainLibs { + case: Case; framework: FrameworkAdapter; sources: Sources; sourceStatus: SourceStatus; diff --git a/x-pack/legacy/plugins/siem/server/saved_objects.ts b/x-pack/legacy/plugins/siem/server/saved_objects.ts index 8311d17f00ce9a..ba2e87499854bf 100644 --- a/x-pack/legacy/plugins/siem/server/saved_objects.ts +++ b/x-pack/legacy/plugins/siem/server/saved_objects.ts @@ -12,10 +12,16 @@ import { timelineSavedObjectType, timelineSavedObjectMappings, } from './lib/timeline/saved_object_mappings'; +import { + caseSavedObjectMappings, + caseCommentSavedObjectMappings, +} from './lib/case/saved_object_mappings'; export { noteSavedObjectType, pinnedEventSavedObjectType, timelineSavedObjectType }; export const savedObjectMappings = { ...timelineSavedObjectMappings, ...noteSavedObjectMappings, ...pinnedEventSavedObjectMappings, + ...caseSavedObjectMappings, + ...caseCommentSavedObjectMappings, }; diff --git a/x-pack/test/api_integration/apis/siem/ip_overview.ts b/x-pack/test/api_integration/apis/siem/ip_overview.ts index bec31a018cac8d..50476d878b220e 100644 --- a/x-pack/test/api_integration/apis/siem/ip_overview.ts +++ b/x-pack/test/api_integration/apis/siem/ip_overview.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { ipOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/ip_overview/index.gql_query'; +import { ipOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/case/index.gql_query'; import { GetIpOverviewQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; From 52e388bfaf05e51ce8c14fb545617a31f18afa53 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 10 Jan 2020 11:56:02 -0700 Subject: [PATCH 02/67] get cases --- .../case/{ => get_case}/index.gql_query.ts | 4 - .../public/containers/case/get_case/index.tsx | 73 ++++++ .../case/get_cases/index.gql_query.ts | 35 +++ .../containers/case/get_cases/index.tsx | 80 +++++++ .../siem/public/containers/case/index.tsx | 76 ------- .../siem/public/graphql/introspection.json | 207 +++++------------- .../plugins/siem/public/graphql/types.ts | 103 ++++++--- .../plugins/siem/public/pages/case/case.tsx | 21 +- .../siem/public/pages/case/case_list.tsx | 46 ++++ .../siem/public/pages/case/case_view.tsx | 61 ++++++ .../siem/server/graphql/case/resolvers.ts | 14 +- .../siem/server/graphql/case/schema.gql.ts | 50 ++--- .../plugins/siem/server/graphql/types.ts | 100 ++++----- .../siem/server/lib/case/saved_object.ts | 36 +-- .../api_integration/apis/siem/ip_overview.ts | 2 +- 15 files changed, 510 insertions(+), 398 deletions(-) rename x-pack/legacy/plugins/siem/public/containers/case/{ => get_case}/index.gql_query.ts (90%) create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/containers/case/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/case_list.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/case_view.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.gql_query.ts similarity index 90% rename from x-pack/legacy/plugins/siem/public/containers/case/index.gql_query.ts rename to x-pack/legacy/plugins/siem/public/containers/case/get_case/index.gql_query.ts index 875853eb413029..aa90ff6a666daa 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.gql_query.ts @@ -14,10 +14,6 @@ export const caseQuery = gql` updated_at version attributes { - assignees { - username - full_name - } case_type created_at created_by { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx new file mode 100644 index 00000000000000..4033c07497ee5f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx @@ -0,0 +1,73 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { GetCaseQuery, CaseSavedObject } from '../../../graphql/types'; +import { inputsModel } from '../../../store'; +import { getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplateProps } from '../../query_template'; + +import { caseQuery } from './index.gql_query'; + +const ID = 'caseQuery'; + +export interface CaseArgs { + id: string; + case: CaseSavedObject; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface CaseProps extends QueryTemplateProps { + caseId: string; + children: (args: CaseArgs) => React.ReactNode; +} + +const CaseComponentQuery = React.memo(({ children, skip, caseId }) => ( + + query={caseQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + caseId, + }} + > + {({ data, loading, refetch }) => { + const init: CaseSavedObject = { + id: caseId, + type: '', + updated_at: '', + version: '', + attributes: { + case_type: '', + created_at: 1234235345, + created_by: { + username: '', + full_name: null, + }, + description: '', + state: 'open', + tags: [], + title: '', + }, + }; + const caseData: CaseSavedObject = getOr(init, 'getCase', data); + return children({ + id: ID, + case: caseData, + loading, + refetch, + }); + }} + +)); + +CaseComponentQuery.displayName = 'CaseComponentQuery'; + +export const CaseQuery = CaseComponentQuery; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts new file mode 100644 index 00000000000000..a7ba9075ca0e54 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts @@ -0,0 +1,35 @@ +/* + * 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 gql from 'graphql-tag'; + +export const casesQuery = gql` + query GetCasesQuery($search: String) { + getCases(search: $search) { + page + per_page + total + saved_objects { + id + type + updated_at + version + attributes { + case_type + created_at + created_by { + username + full_name + } + description + state + tags + title + } + } + } + } +`; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx new file mode 100644 index 00000000000000..7a9035e4e715ce --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx @@ -0,0 +1,80 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { GetCasesQuery, CasesSavedObjects } from '../../../graphql/types'; +import { inputsModel } from '../../../store'; +import { getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplateProps } from '../../query_template'; + +import { casesQuery } from './index.gql_query'; + +const ID = 'casesQuery'; + +export interface CasesArgs { + id: string; + cases: CasesSavedObjects; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface CasesProps extends QueryTemplateProps { + children: (args: CasesArgs) => React.ReactNode; + search?: string; +} + +const CasesComponentQuery = React.memo(({ id = ID, children, skip, search }) => ( + + query={casesQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + search, + }} + > + {({ data, loading, refetch }) => { + const init: CasesSavedObjects = { + page: 0, + per_page: 0, + total: 0, + saved_objects: [ + { + id: '000', + type: '', + updated_at: '', + version: '', + attributes: { + case_type: '', + created_at: 1234235345, + created_by: { + username: '', + full_name: null, + }, + description: '', + state: 'open', + tags: [], + title: '', + }, + }, + ], + }; + const caseData: CasesSavedObjects = getOr(init, 'getCases', data); + return children({ + id: ID, + cases: caseData, + loading, + refetch, + }); + }} + +)); + +CasesComponentQuery.displayName = 'CasesComponentQuery'; + +export const CasesQuery = CasesComponentQuery; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/index.tsx deleted file mode 100644 index 47dfd1b97c946f..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { GetCaseQuery, CaseSavedObject } from '../../graphql/types'; -import { inputsModel } from '../../store'; -import { getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { caseQuery } from './index.gql_query'; - -const ID = 'caseQuery'; - -export interface CaseArgs { - id: string; - caseData: CaseSavedObject; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface CaseProps extends QueryTemplateProps { - caseId: string; - children: (args: CaseArgs) => React.ReactNode; -} - -const CaseComponentQuery = React.memo( - ({ id = ID, children, skip, sourceId, caseId }) => ( - - query={caseQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - caseId, - }} - > - {({ data, loading, refetch }) => { - const init: CaseSavedObject = { - id: caseId, - type: '', - updated_at: '', - version: '', - attributes: { - assignees: [], - case_type: '', - created_at: 1234235345, - created_by: { - username: '', - full_name: null, - }, - description: '', - state: 'open', - tags: [], - title: '', - }, - }; - const caseData: CaseSavedObject = getOr(init, 'getCase', data); - return children({ - id, - caseData, - loading, - refetch, - }); - }} - - ) -); - -CaseComponentQuery.displayName = 'CaseComponentQuery'; - -export const CaseQuery = CaseComponentQuery; diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 2ab5530db5005c..89c0a816403c61 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -33,32 +33,20 @@ "deprecationReason": null }, { - "name": "getAllCases", + "name": "getCases", "description": "", "args": [ - { - "name": "pageInfo", - "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "PageInfoCase", "ofType": null }, - "defaultValue": null - }, { "name": "search", "description": "", "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null - }, - { - "name": "sort", - "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "SortCase", "ofType": null }, - "defaultValue": null } ], "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "ResponseCases", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "CasesSavedObjects", "ofType": null } }, "isDeprecated": false, "deprecationReason": null @@ -405,22 +393,6 @@ "name": "CaseResult", "description": "", "fields": [ - { - "name": "assignees", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "ElasticUser", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "case_type", "description": "", @@ -515,37 +487,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "ElasticUser", - "description": "", - "fields": [ - { - "name": "username", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "full_name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "SCALAR", "name": "String", @@ -567,128 +508,90 @@ "possibleTypes": null }, { - "kind": "INPUT_OBJECT", - "name": "PageInfoCase", + "kind": "OBJECT", + "name": "ElasticUser", "description": "", - "fields": null, - "inputFields": [ + "fields": [ { - "name": "pageIndex", + "name": "username", "description": "", + "args": [], "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null }, { - "name": "pageSize", + "name": "full_name", "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], - "interfaces": null, + "inputFields": null, + "interfaces": [], "enumValues": null, "possibleTypes": null }, { - "kind": "INPUT_OBJECT", - "name": "SortCase", + "kind": "OBJECT", + "name": "CasesSavedObjects", "description": "", - "fields": null, - "inputFields": [ + "fields": [ { - "name": "sortField", + "name": "saved_objects", "description": "", + "args": [], "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "ENUM", "name": "SortFieldCase", "ofType": null } + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "CaseSavedObject", "ofType": null } + } }, - "defaultValue": null + "isDeprecated": false, + "deprecationReason": null }, { - "name": "sortOrder", + "name": "page", "description": "", + "args": [], "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SortFieldCase", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "updatedBy", - "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "updated", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "Direction", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { "name": "asc", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "desc", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "ResponseCases", - "description": "", - "fields": [ { - "name": "cases", + "name": "per_page", "description": "", "args": [], "type": { "kind": "NON_NULL", "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CaseResult", "ofType": null } - } - } + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "totalCount", + "name": "total", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null } @@ -873,6 +776,19 @@ ], "possibleTypes": null }, + { + "kind": "ENUM", + "name": "Direction", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "asc", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "desc", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "ResponseNotes", @@ -12334,23 +12250,6 @@ ], "possibleTypes": null }, - { - "kind": "INPUT_OBJECT", - "name": "CaseInput", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "caseId", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "EcsEdges", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 65ae8240f6d44d..ac0ea45a6de0a4 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -8,18 +8,6 @@ export type Maybe = T | null; -export interface PageInfoCase { - pageIndex: number; - - pageSize: number; -} - -export interface SortCase { - sortField: SortFieldCase; - - sortOrder: Direction; -} - export interface PageInfoNote { pageIndex: number; @@ -269,10 +257,6 @@ export interface SortTimelineInput { sortDirection?: Maybe; } -export interface CaseInput { - caseId?: Maybe; -} - export interface FavoriteTimelineInput { fullName?: Maybe; @@ -281,7 +265,7 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldCase { +export enum SortFieldNote { updatedBy = 'updatedBy', updated = 'updated', } @@ -291,11 +275,6 @@ export enum Direction { desc = 'desc', } -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', -} - export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -401,7 +380,7 @@ export type EsValue = any; export interface Query { getCase: CaseSavedObject; - getAllCases: ResponseCases; + getCases: CasesSavedObjects; getNote: NoteResult; @@ -435,8 +414,6 @@ export interface CaseSavedObject { } export interface CaseResult { - assignees: (Maybe)[]; - case_type: string; created_at: number; @@ -458,10 +435,14 @@ export interface ElasticUser { full_name?: Maybe; } -export interface ResponseCases { - cases: CaseResult[]; +export interface CasesSavedObjects { + saved_objects: (Maybe)[]; - totalCount?: Maybe; + page: number; + + per_page: number; + + total: number; } export interface NoteResult { @@ -2183,12 +2164,8 @@ export interface HostFields { export interface GetCaseQueryArgs { caseId: string; } -export interface GetAllCasesQueryArgs { - pageInfo?: Maybe; - +export interface GetCasesQueryArgs { search?: Maybe; - - sort?: Maybe; } export interface GetNoteQueryArgs { id: string; @@ -2859,8 +2836,6 @@ export namespace GetCaseQuery { export type Attributes = { __typename?: 'CaseResult'; - assignees: (Maybe)[]; - case_type: string; created_at: number; @@ -2876,13 +2851,69 @@ export namespace GetCaseQuery { title: string; }; - export type Assignees = { + export type CreatedBy = { __typename?: 'ElasticUser'; username: string; full_name: Maybe; }; +} + +export namespace GetCasesQuery { + export type Variables = { + search?: Maybe; + }; + + export type Query = { + __typename?: 'Query'; + + getCases: GetCases; + }; + + export type GetCases = { + __typename?: 'CasesSavedObjects'; + + page: number; + + per_page: number; + + total: number; + + saved_objects: (Maybe)[]; + }; + + export type SavedObjects = { + __typename?: 'CaseSavedObject'; + + id: string; + + type: string; + + updated_at: string; + + version: string; + + attributes: Attributes; + }; + + export type Attributes = { + __typename?: 'CaseResult'; + + case_type: string; + + created_at: number; + + created_by: CreatedBy; + + description: string; + + state: string; + + tags: (Maybe)[]; + + title: string; + }; export type CreatedBy = { __typename?: 'ElasticUser'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 5bb4cd26240c17..2c43a1fba203d4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import React from 'react'; import chrome from 'ui/chrome'; @@ -14,7 +14,8 @@ import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { CaseQuery } from '../../containers/case'; +import { CaseList } from './case_list'; +import { CaseView } from './case_view'; import * as i18n from './translations'; const basePath = chrome.getBasePath(); @@ -40,18 +41,10 @@ export const CaseComponent = React.memo(() => { {({ indicesExist }) => indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {children => ( -
-

{`Case: ${children.caseData.attributes.title}`}

-
    -
  • {`Description: ${children.caseData.attributes.description}`}
  • -
  • {`Type: ${children.caseData.attributes.case_type}`}
  • -
  • {`State: ${children.caseData.attributes.state}`}
  • -
-
- )} -
+ + + +
) : ( ( + + + {children => ( + +

+ +

+
    + {children.cases.saved_objects.map( + theCase => + theCase && ( +
  • +
      +
    • {`Type: ${theCase.attributes.title}`}
    • +
    • {`Description: ${theCase.attributes.description}`}
    • +
    • {`Type: ${theCase.attributes.case_type}`}
    • +
    • {`State: ${theCase.attributes.state}`}
    • +
    +
  • + ) + )} +
+
+ )} +
+
+)); + +CaseList.displayName = 'CaseList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_view.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_view.tsx new file mode 100644 index 00000000000000..afc409435d0b44 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_view.tsx @@ -0,0 +1,61 @@ +/* + * 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 from 'react'; +import { EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { CaseQuery } from '../../containers/case/get_case'; + +export const CaseView = React.memo(() => ( + + + {children => ( + +

{children.case.attributes.title}

+
+
+ +
+
{children.case.attributes.description}
+
+ +
+
{children.case.attributes.case_type}
+
+ +
+
{children.case.attributes.state}
+
+ +
+
{children.case.updated_at}
+
+ +
+
{children.case.attributes.created_at}
+
+ +
+
{children.case.attributes.created_by.username}
+
+ +
+
+
    + {children.case.attributes.tags.map((tag, key) => ( +
  • {tag}
  • + ))} +
+
+
+
+ )} +
+
+)); + +CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts index eaa4e6e9c81ac9..739cbbd3abf930 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts @@ -11,7 +11,7 @@ import { Case } from '../../lib/case/saved_object'; export type QueryCaseResolver = AppResolverOf; export type QueryAllCaseResolver = AppResolverWithFields< - QueryResolvers.GetAllCasesResolver, + QueryResolvers.GetCasesResolver, 'totalCount' | 'Case' >; @@ -26,7 +26,7 @@ export const createCaseResolvers = ( ): { Query: { getCase: QueryCaseResolver; - getAllCases: QueryAllCaseResolver; + getCases: QueryAllCaseResolver; }; Mutation: { deleteCase: MutationDeleteCaseResolver; @@ -36,12 +36,12 @@ export const createCaseResolvers = ( async getCase(root, args, { req }) { return libs.case.getCase(req, args.caseId); }, - async getAllCases(root, args, { req }) { - return libs.case.getAllCases( + async getCases(root, args, { req }) { + return libs.case.getCases( req, - args.pageInfo || null, - args.search || null, - args.sort || null + // args.pageInfo || null, + args.search || null + // args.sort || null ); }, }, diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts index 28a4898f33e731..671baaa3e6d8c7 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts @@ -7,29 +7,6 @@ import gql from 'graphql-tag'; export const caseSchema = gql` - ############### - #### INPUT #### - ############### - - input CaseInput { - caseId: String - } - - input PageInfoCase { - pageIndex: Float! - pageSize: Float! - } - - enum SortFieldCase { - updatedBy - updated - } - - input SortCase { - sortField: SortFieldCase! - sortOrder: Direction! - } - ############### #### QUERY #### ############### @@ -38,7 +15,6 @@ export const caseSchema = gql` full_name: String } type CaseResult { - assignees: [ElasticUser]! case_type: String! created_at: Float! created_by: ElasticUser! @@ -56,9 +32,11 @@ export const caseSchema = gql` version: String! } - type ResponseCases { - cases: [CaseResult!]! - totalCount: Float + type CasesSavedObjects { + saved_objects: [CaseSavedObject]! + page: Float! + per_page: Float! + total: Float! } ######################### @@ -67,10 +45,26 @@ export const caseSchema = gql` extend type Query { getCase(caseId: ID!): CaseSavedObject! - getAllCases(pageInfo: PageInfoCase, search: String, sort: SortCase): ResponseCases! + getCases(search: String): CasesSavedObjects! } extend type Mutation { deleteCase(id: [ID!]!): Boolean } `; + +// +// input PageInfoCase { +// pageIndex: Float! +// pageSize: Float! +// } +// +// enum SortFieldCase { +// updatedBy +// updated +// } +// +// input SortCase { +// sortField: SortFieldCase! +// sortOrder: Direction! +// } diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index 90c174fdd51c82..89bf4000b4f7ce 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -10,18 +10,6 @@ import { SiemContext } from '../lib/types'; export type Maybe = T | null; -export interface PageInfoCase { - pageIndex: number; - - pageSize: number; -} - -export interface SortCase { - sortField: SortFieldCase; - - sortOrder: Direction; -} - export interface PageInfoNote { pageIndex: number; @@ -271,10 +259,6 @@ export interface SortTimelineInput { sortDirection?: Maybe; } -export interface CaseInput { - caseId?: Maybe; -} - export interface FavoriteTimelineInput { fullName?: Maybe; @@ -283,7 +267,7 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldCase { +export enum SortFieldNote { updatedBy = 'updatedBy', updated = 'updated', } @@ -293,11 +277,6 @@ export enum Direction { desc = 'desc', } -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', -} - export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -403,7 +382,7 @@ export type EsValue = any; export interface Query { getCase: CaseSavedObject; - getAllCases: ResponseCases; + getCases: CasesSavedObjects; getNote: NoteResult; @@ -437,8 +416,6 @@ export interface CaseSavedObject { } export interface CaseResult { - assignees: (Maybe)[]; - case_type: string; created_at: number; @@ -460,10 +437,14 @@ export interface ElasticUser { full_name?: Maybe; } -export interface ResponseCases { - cases: CaseResult[]; +export interface CasesSavedObjects { + saved_objects: (Maybe)[]; - totalCount?: Maybe; + page: number; + + per_page: number; + + total: number; } export interface NoteResult { @@ -2185,12 +2166,8 @@ export interface HostFields { export interface GetCaseQueryArgs { caseId: string; } -export interface GetAllCasesQueryArgs { - pageInfo?: Maybe; - +export interface GetCasesQueryArgs { search?: Maybe; - - sort?: Maybe; } export interface GetNoteQueryArgs { id: string; @@ -2582,7 +2559,7 @@ export namespace QueryResolvers { export interface Resolvers { getCase?: GetCaseResolver; - getAllCases?: GetAllCasesResolver; + getCases?: GetCasesResolver; getNote?: GetNoteResolver; @@ -2617,17 +2594,13 @@ export namespace QueryResolvers { caseId: string; } - export type GetAllCasesResolver< - R = ResponseCases, + export type GetCasesResolver< + R = CasesSavedObjects, Parent = {}, TContext = SiemContext - > = Resolver; - export interface GetAllCasesArgs { - pageInfo?: Maybe; - + > = Resolver; + export interface GetCasesArgs { search?: Maybe; - - sort?: Maybe; } export type GetNoteResolver = Resolver< @@ -2763,8 +2736,6 @@ export namespace CaseSavedObjectResolvers { export namespace CaseResultResolvers { export interface Resolvers { - assignees?: AssigneesResolver<(Maybe)[], TypeParent, TContext>; - case_type?: CaseTypeResolver; created_at?: CreatedAtResolver; @@ -2780,11 +2751,6 @@ export namespace CaseResultResolvers { title?: TitleResolver; } - export type AssigneesResolver< - R = (Maybe)[], - Parent = CaseResult, - TContext = SiemContext - > = Resolver; export type CaseTypeResolver = Resolver< R, Parent, @@ -2841,21 +2807,35 @@ export namespace ElasticUserResolvers { > = Resolver; } -export namespace ResponseCasesResolvers { - export interface Resolvers { - cases?: CasesResolver; +export namespace CasesSavedObjectsResolvers { + export interface Resolvers { + saved_objects?: SavedObjectsResolver<(Maybe)[], TypeParent, TContext>; - totalCount?: TotalCountResolver, TypeParent, TContext>; + page?: PageResolver; + + per_page?: PerPageResolver; + + total?: TotalResolver; } - export type CasesResolver< - R = CaseResult[], - Parent = ResponseCases, + export type SavedObjectsResolver< + R = (Maybe)[], + Parent = CasesSavedObjects, TContext = SiemContext > = Resolver; - export type TotalCountResolver< - R = Maybe, - Parent = ResponseCases, + export type PageResolver< + R = number, + Parent = CasesSavedObjects, + TContext = SiemContext + > = Resolver; + export type PerPageResolver< + R = number, + Parent = CasesSavedObjects, + TContext = SiemContext + > = Resolver; + export type TotalResolver< + R = number, + Parent = CasesSavedObjects, TContext = SiemContext > = Resolver; } @@ -9021,7 +9001,7 @@ export type IResolvers = { CaseSavedObject?: CaseSavedObjectResolvers.Resolvers; CaseResult?: CaseResultResolvers.Resolvers; ElasticUser?: ElasticUserResolvers.Resolvers; - ResponseCases?: ResponseCasesResolvers.Resolvers; + CasesSavedObjects?: CasesSavedObjectsResolvers.Resolvers; NoteResult?: NoteResultResolvers.Resolvers; ResponseNotes?: ResponseNotesResolvers.Resolvers; PinnedEvent?: PinnedEventResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts index 17e98e16c99575..7944d74b40c0b6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; -import { PageInfoCase, CaseSavedObject, ResponseCases, SortCase } from '../../graphql/types'; +import { CaseSavedObject, CasesSavedObjects } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; import { caseSavedObjectType } from './saved_object_mappings'; export class Case { @@ -21,20 +21,20 @@ export class Case { return this.getSavedCase(request, caseId); } - public async getAllCases( + public async getCases( request: FrameworkRequest, - pageInfo: PageInfoCase | null, - search: string | null, - sort: SortCase | null - ): Promise { + // pageInfo: PageInfoCase | null, + search: string | null + // sort: SortCase | null + ): Promise { const options: SavedObjectsFindOptions = { type: caseSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, + // perPage: pageInfo != null ? pageInfo.pageSize : undefined, + // page: pageInfo != null ? pageInfo.pageIndex : undefined, search: search != null ? search : undefined, - searchFields: ['note'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, + // searchFields: ['note'], + // sortField: sort != null ? sort.sortField : undefined, + // sortOrder: sort != null ? sort.sortOrder : undefined, }; return this.getAllSavedCase(request, options); } @@ -57,13 +57,13 @@ export class Case { private async getAllSavedCase(request: FrameworkRequest, options: SavedObjectsFindOptions) { const savedObjectsClient = request.context.core.savedObjects.client; const savedObjects = await savedObjectsClient.find(options); - - return { - totalCount: savedObjects.total, - cases: savedObjects.saved_objects.map(savedObject => - convertSavedObjectToSavedCase(savedObject) - ), - }; + return savedObjects; + // return { + // totalCount: savedObjects.total, + // cases: savedObjects.saved_objects.map(savedObject => + // convertSavedObjectToSavedCase(savedObject) + // ), + // }; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/test/api_integration/apis/siem/ip_overview.ts b/x-pack/test/api_integration/apis/siem/ip_overview.ts index 50476d878b220e..bec31a018cac8d 100644 --- a/x-pack/test/api_integration/apis/siem/ip_overview.ts +++ b/x-pack/test/api_integration/apis/siem/ip_overview.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { ipOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/case/index.gql_query'; +import { ipOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/ip_overview/index.gql_query'; import { GetIpOverviewQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; From 6ccb205d74d4032c37c91b40d3d4da216bccaa96 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 10 Jan 2020 15:08:00 -0700 Subject: [PATCH 03/67] cases table --- .../page/case/case_view/index.tsx} | 2 +- .../page/case/cases_table/columns.tsx | 64 +++++++++++++++++++ .../page/case/cases_table/index.tsx | 50 +++++++++++++++ .../components/paginated_table/index.tsx | 2 + .../plugins/siem/public/pages/case/case.tsx | 10 ++- .../siem/public/pages/case/case_list.tsx | 46 ------------- 6 files changed, 121 insertions(+), 53 deletions(-) rename x-pack/legacy/plugins/siem/public/{pages/case/case_view.tsx => components/page/case/case_view/index.tsx} (97%) create mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/case_list.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_view.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/pages/case/case_view.tsx rename to x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx index afc409435d0b44..a14488090bd1e1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CaseQuery } from '../../containers/case/get_case'; +import { CaseQuery } from '../../../../containers/case/get_case'; export const CaseView = React.memo(() => ( diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx new file mode 100644 index 00000000000000..9825a6c0770e49 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx @@ -0,0 +1,64 @@ +/* + * 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 from 'react'; +import { getEmptyTagValue } from '../../../empty_value'; +import { Columns } from '../../../paginated_table'; +import { CaseSavedObject } from '../../../../graphql/types'; +import { FormattedRelativePreferenceDate } from '../../../formatted_date'; + +export type CasesColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); + +export const getCasesColumns = (): CasesColumns => [ + { + field: 'attributes.title', + name: 'Case Title', + render: title => renderStringField(title), + }, + { + field: 'id', + name: 'Case Id', + render: id => renderStringField(id), + }, + { + field: 'attributes.created_at', + name: 'Created at', + render: createdAt => { + if (createdAt != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'attributes.created_by.username', + name: 'Created by', + render: createdBy => renderStringField(createdBy), + }, + { + field: 'updated_at', + name: 'Last updated', + render: updatedAt => { + if (updatedAt != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'attributes.state', + name: 'State', + render: state => renderStringField(state), + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx new file mode 100644 index 00000000000000..cbc78f411c4b29 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx @@ -0,0 +1,50 @@ +/* + * 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 } from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { PaginatedTable } from '../../../paginated_table'; + +import { CasesQuery } from '../../../../containers/case/get_cases'; +import { getCasesColumns } from './columns'; + +export const CasesTable = React.memo(() => { + const [activePage, updateActivePage] = useState(0); + const [limit, updateLimitPagination] = useState(10); + + return ( + + + {children => ( + + } + headerUnit={'cases'} + id={children.id} + limit={limit} + loading={false} + loadPage={page => updateActivePage(page)} + pageOfItems={children.cases.saved_objects} + showMorePagesIndicator={true} + totalCount={children.cases.total} + updateActivePage={updateActivePage} + updateLimitPagination={updateLimitPagination} + /> + )} + + + ); +}); + +CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index a552ac270a6fe9..fe6022c87f77e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -22,6 +22,7 @@ import React, { memo, useState, useEffect, useCallback, ComponentType } from 're import styled from 'styled-components'; import { AuthTableColumns } from '../page/hosts/authentications_table'; +import { CasesColumns } from '../page/case/cases_table/columns'; import { HostsTableColumns } from '../page/hosts/hosts_table'; import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; @@ -71,6 +72,7 @@ declare type HostsTableColumnsTest = [ declare type BasicTableColumns = | AuthTableColumns + | CasesColumns | HostsTableColumns | HostsTableColumnsTest | NetworkDnsColumns diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 2c43a1fba203d4..6ba310081b5258 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; import chrome from 'ui/chrome'; @@ -14,8 +14,8 @@ import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { CaseList } from './case_list'; -import { CaseView } from './case_view'; +import { CasesTable } from '../../components/page/case/cases_table'; +import { CaseView } from '../../components/page/case/case_view'; import * as i18n from './translations'; const basePath = chrome.getBasePath(); @@ -42,9 +42,7 @@ export const CaseComponent = React.memo(() => { indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - + ) : ( ( - - - {children => ( - -

- -

-
    - {children.cases.saved_objects.map( - theCase => - theCase && ( -
  • -
      -
    • {`Type: ${theCase.attributes.title}`}
    • -
    • {`Description: ${theCase.attributes.description}`}
    • -
    • {`Type: ${theCase.attributes.case_type}`}
    • -
    • {`State: ${theCase.attributes.state}`}
    • -
    -
  • - ) - )} -
-
- )} -
-
-)); - -CaseList.displayName = 'CaseList'; From 39e1cf7c0854477f0b7838c798835ede9d6cbe4f Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 16 Jan 2020 08:07:32 -0700 Subject: [PATCH 04/67] add sorting --- .../page/case/cases_table/columns.tsx | 3 +- .../page/case/cases_table/index.tsx | 32 +---- .../page/case/cases_table/table.tsx | 45 +++++++ .../case/get_cases/index.gql_query.ts | 4 +- .../containers/case/get_cases/index.tsx | 10 +- .../siem/public/graphql/introspection.json | 117 ++++++++++++++++-- .../plugins/siem/public/graphql/types.ts | 28 ++++- .../siem/server/graphql/case/resolvers.ts | 7 +- .../siem/server/graphql/case/schema.gql.ts | 15 ++- .../plugins/siem/server/graphql/types.ts | 30 ++++- .../siem/server/lib/case/saved_object.ts | 36 +++--- 11 files changed, 252 insertions(+), 75 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx index 9825a6c0770e49..7ccd5c3dfb0e46 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx @@ -12,8 +12,8 @@ import { FormattedRelativePreferenceDate } from '../../../formatted_date'; export type CasesColumns = [ Columns, Columns, - Columns, Columns, + Columns, Columns, Columns ]; @@ -34,6 +34,7 @@ export const getCasesColumns = (): CasesColumns => [ { field: 'attributes.created_at', name: 'Created at', + sortable: true, render: createdAt => { if (createdAt != null) { return ; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx index cbc78f411c4b29..3b9e83d85e4135 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx @@ -4,44 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { PaginatedTable } from '../../../paginated_table'; import { CasesQuery } from '../../../../containers/case/get_cases'; -import { getCasesColumns } from './columns'; +import { CasesPaginatedTable } from './table'; export const CasesTable = React.memo(() => { - const [activePage, updateActivePage] = useState(0); - const [limit, updateLimitPagination] = useState(10); return ( - {children => ( - - } - headerUnit={'cases'} - id={children.id} - limit={limit} - loading={false} - loadPage={page => updateActivePage(page)} - pageOfItems={children.cases.saved_objects} - showMorePagesIndicator={true} - totalCount={children.cases.total} - updateActivePage={updateActivePage} - updateLimitPagination={updateLimitPagination} - /> - )} + {children => } ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx new file mode 100644 index 00000000000000..bc23427ee1e8b2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx @@ -0,0 +1,45 @@ +/* + * 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 } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { PaginatedTable } from '../../../paginated_table'; + +import { getCasesColumns } from './columns'; +import { CasesSavedObjects } from '../../../../graphql/types'; + +interface CasesTableProps { + id: string; + cases: CasesSavedObjects; +} + +export const CasesPaginatedTable = React.memo(({ id, cases }: CasesTableProps) => { + const [activePage, updateActivePage] = useState(0); + const [limit, updateLimitPagination] = useState(10); + + return ( + + } + headerUnit={'cases'} + id={id} + limit={limit} + loading={false} + loadPage={page => updateActivePage(page)} + pageOfItems={cases.saved_objects} + showMorePagesIndicator={true} + totalCount={cases.total} + updateActivePage={updateActivePage} + updateLimitPagination={updateLimitPagination} + /> + ); +}); + +CasesPaginatedTable.displayName = 'CasesPaginatedTable'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts index a7ba9075ca0e54..aead147b5c87b8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts @@ -7,8 +7,8 @@ import gql from 'graphql-tag'; export const casesQuery = gql` - query GetCasesQuery($search: String) { - getCases(search: $search) { + query GetCasesQuery($pageInfo: PageInfoCase!, $search: String, $sort: SortCase) { + getCases(pageInfo: $pageInfo, search: $search, sort: $sort) { page per_page total diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx index 7a9035e4e715ce..ad3248406c0478 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx @@ -7,7 +7,7 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { Query } from 'react-apollo'; -import { GetCasesQuery, CasesSavedObjects } from '../../../graphql/types'; +import { GetCasesQuery, CasesSavedObjects, Direction, SortFieldCase } from '../../../graphql/types'; import { inputsModel } from '../../../store'; import { getDefaultFetchPolicy } from '../../helpers'; import { QueryTemplateProps } from '../../query_template'; @@ -35,7 +35,15 @@ const CasesComponentQuery = React.memo(({ id = ID, children, skip, s notifyOnNetworkStatusChange skip={skip} variables={{ + pageInfo: { + pageIndex: 1, + pageSize: 5, + }, search, + sort: { + sortField: SortFieldCase.created_at, + sortOrder: Direction.asc, + }, }} > {({ data, loading, refetch }) => { diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 6dbffe96102eba..2bcf11e5cf5b65 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -36,11 +36,23 @@ "name": "getCases", "description": "", "args": [ + { + "name": "pageInfo", + "description": "", + "type": { "kind": "INPUT_OBJECT", "name": "PageInfoCase", "ofType": null }, + "defaultValue": null + }, { "name": "search", "description": "", "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null + }, + { + "name": "sort", + "description": "", + "type": { "kind": "INPUT_OBJECT", "name": "SortCase", "ofType": null }, + "defaultValue": null } ], "type": { @@ -538,6 +550,98 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "PageInfoCase", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "pageIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "pageSize", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SortCase", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "sortField", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "SortFieldCase", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "sortOrder", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "SortFieldCase", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "created_at", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "Direction", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "asc", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "desc", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "CasesSavedObjects", @@ -776,19 +880,6 @@ ], "possibleTypes": null }, - { - "kind": "ENUM", - "name": "Direction", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { "name": "asc", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "desc", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, { "kind": "OBJECT", "name": "ResponseNotes", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 00e1f977fbb450..69a5bb03cd38df 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -8,6 +8,18 @@ export type Maybe = T | null; +export interface PageInfoCase { + pageIndex: number; + + pageSize: number; +} + +export interface SortCase { + sortField: SortFieldCase; + + sortOrder: Direction; +} + export interface PageInfoNote { pageIndex: number; @@ -267,9 +279,8 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', +export enum SortFieldCase { + created_at = 'created_at', } export enum Direction { @@ -277,6 +288,11 @@ export enum Direction { desc = 'desc', } +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', +} + export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -2241,7 +2257,11 @@ export interface GetCaseQueryArgs { caseId: string; } export interface GetCasesQueryArgs { + pageInfo?: Maybe; + search?: Maybe; + + sort?: Maybe; } export interface GetNoteQueryArgs { id: string; @@ -2792,7 +2812,9 @@ export namespace GetCaseQuery { export namespace GetCasesQuery { export type Variables = { + pageInfo: PageInfoCase; search?: Maybe; + sort?: Maybe; }; export type Query = { diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts index 739cbbd3abf930..94ab2095131d3f 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts @@ -37,12 +37,7 @@ export const createCaseResolvers = ( return libs.case.getCase(req, args.caseId); }, async getCases(root, args, { req }) { - return libs.case.getCases( - req, - // args.pageInfo || null, - args.search || null - // args.sort || null - ); + return libs.case.getCases(req, args.pageInfo || null, args.search || null, args.sort || null); }, }, Mutation: { diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts index 671baaa3e6d8c7..4cf2782f9b5abe 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts @@ -7,6 +7,19 @@ import gql from 'graphql-tag'; export const caseSchema = gql` + input PageInfoCase { + pageIndex: Float! + pageSize: Float! + } + + enum SortFieldCase { + created_at + } + + input SortCase { + sortField: SortFieldCase! + sortOrder: Direction! + } ############### #### QUERY #### ############### @@ -45,7 +58,7 @@ export const caseSchema = gql` extend type Query { getCase(caseId: ID!): CaseSavedObject! - getCases(search: String): CasesSavedObjects! + getCases(pageInfo: PageInfoCase, search: String, sort: SortCase): CasesSavedObjects! } extend type Mutation { diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index e19c772ebb9af9..82974b3a30da0c 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -10,6 +10,18 @@ import { SiemContext } from '../lib/types'; export type Maybe = T | null; +export interface PageInfoCase { + pageIndex: number; + + pageSize: number; +} + +export interface SortCase { + sortField: SortFieldCase; + + sortOrder: Direction; +} + export interface PageInfoNote { pageIndex: number; @@ -269,9 +281,8 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', +export enum SortFieldCase { + created_at = 'created_at', } export enum Direction { @@ -279,6 +290,11 @@ export enum Direction { desc = 'desc', } +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', +} + export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -2243,7 +2259,11 @@ export interface GetCaseQueryArgs { caseId: string; } export interface GetCasesQueryArgs { + pageInfo?: Maybe; + search?: Maybe; + + sort?: Maybe; } export interface GetNoteQueryArgs { id: string; @@ -2686,7 +2706,11 @@ export namespace QueryResolvers { TContext = SiemContext > = Resolver; export interface GetCasesArgs { + pageInfo?: Maybe; + search?: Maybe; + + sort?: Maybe; } export type GetNoteResolver = Resolver< diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts index 7944d74b40c0b6..13422b69069361 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts @@ -5,9 +5,10 @@ */ import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; -import { CaseSavedObject, CasesSavedObjects } from '../../graphql/types'; +import { CaseSavedObject, CasesSavedObjects, PageInfoCase, SortCase } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; import { caseSavedObjectType } from './saved_object_mappings'; + export class Case { public async deleteCase(request: FrameworkRequest, noteIds: string[]) { const savedObjectsClient = request.context.core.savedObjects.client; @@ -23,19 +24,21 @@ export class Case { public async getCases( request: FrameworkRequest, - // pageInfo: PageInfoCase | null, - search: string | null - // sort: SortCase | null + pageInfo: PageInfoCase | null, + search: string | null, + sort: SortCase | null ): Promise { const options: SavedObjectsFindOptions = { type: caseSavedObjectType, - // perPage: pageInfo != null ? pageInfo.pageSize : undefined, - // page: pageInfo != null ? pageInfo.pageIndex : undefined, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, search: search != null ? search : undefined, - // searchFields: ['note'], - // sortField: sort != null ? sort.sortField : undefined, - // sortOrder: sort != null ? sort.sortOrder : undefined, + searchFields: ['tags'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, }; + + console.log('getCases return') return this.getAllSavedCase(request, options); } @@ -55,15 +58,16 @@ export class Case { } private async getAllSavedCase(request: FrameworkRequest, options: SavedObjectsFindOptions) { + console.log('getAllSavedCase start', options) const savedObjectsClient = request.context.core.savedObjects.client; const savedObjects = await savedObjectsClient.find(options); - return savedObjects; - // return { - // totalCount: savedObjects.total, - // cases: savedObjects.saved_objects.map(savedObject => - // convertSavedObjectToSavedCase(savedObject) - // ), - // }; + console.log('getAllSavedCase return') + return { + ...savedObjects, + saved_objects: savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedCase(savedObject) + ), + }; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any From 6da190541b7a11fb2843148cd21db4d08ddc6599 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 16 Jan 2020 11:26:59 -0700 Subject: [PATCH 05/67] table working better --- .../page/case/cases_table/table.tsx | 131 ++++++++++++---- .../containers/case/get_cases/index.tsx | 147 +++++++++++------- .../siem/public/graphql/introspection.json | 4 +- .../plugins/siem/public/graphql/types.ts | 4 +- .../plugins/siem/public/store/case/actions.ts | 15 ++ .../plugins/siem/public/store/case/index.ts | 12 ++ .../plugins/siem/public/store/case/model.ts | 37 +++++ .../plugins/siem/public/store/case/reducer.ts | 44 ++++++ .../siem/public/store/case/selectors.ts | 12 ++ .../legacy/plugins/siem/public/store/model.ts | 5 +- .../plugins/siem/public/store/reducer.ts | 4 + .../siem/server/graphql/case/schema.gql.ts | 4 +- .../plugins/siem/server/graphql/types.ts | 4 +- .../siem/server/lib/case/saved_object.ts | 6 +- 14 files changed, 327 insertions(+), 102 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/store/case/actions.ts create mode 100644 x-pack/legacy/plugins/siem/public/store/case/index.ts create mode 100644 x-pack/legacy/plugins/siem/public/store/case/model.ts create mode 100644 x-pack/legacy/plugins/siem/public/store/case/reducer.ts create mode 100644 x-pack/legacy/plugins/siem/public/store/case/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx index bc23427ee1e8b2..00e5a79efee38a 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx @@ -4,42 +4,113 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useCallback } from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PaginatedTable } from '../../../paginated_table'; +import { ActionCreator } from 'typescript-fsa'; +import { Criteria, PaginatedTable } from '../../../paginated_table'; import { getCasesColumns } from './columns'; -import { CasesSavedObjects } from '../../../../graphql/types'; +import { CasesSavedObjects, Direction, SortCase } from '../../../../graphql/types'; +import { caseActions, caseSelectors, caseModel } from '../../../../store/case'; +import { State } from '../../../../store'; interface CasesTableProps { id: string; cases: CasesSavedObjects; + loading: boolean; } -export const CasesPaginatedTable = React.memo(({ id, cases }: CasesTableProps) => { - const [activePage, updateActivePage] = useState(0); - const [limit, updateLimitPagination] = useState(10); - - return ( - - } - headerUnit={'cases'} - id={id} - limit={limit} - loading={false} - loadPage={page => updateActivePage(page)} - pageOfItems={cases.saved_objects} - showMorePagesIndicator={true} - totalCount={cases.total} - updateActivePage={updateActivePage} - updateLimitPagination={updateLimitPagination} - /> - ); -}); - -CasesPaginatedTable.displayName = 'CasesPaginatedTable'; +interface CasesTableReduxProps { + activePage: number; + limit: number; + sort: SortCase; +} + +interface CasesTableDispatchProps { + updateCaseTable: ActionCreator<{ + tableType: caseModel.CaseTableType; + updates: caseModel.TableUpdates; + }>; +} + +type CasesPaginatedTableProps = CasesTableReduxProps & CasesTableProps & CasesTableDispatchProps; + +export const CasesPaginatedTableComponent = React.memo( + ({ id, cases, loading, sort, activePage, limit, updateCaseTable }: CasesPaginatedTableProps) => { + const tableType = caseModel.CaseTableType.cases; + const updateActivePage = useCallback( + newPage => + updateCaseTable({ + tableType, + updates: { activePage: newPage }, + }), + [updateCaseTable, tableType] + ); + + const updateLimitPagination = useCallback( + newLimit => + updateCaseTable({ + tableType, + updates: { limit: newLimit }, + }), + [updateCaseTable, tableType] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + console.log('SORT', { + criteria: criteria.sort, + sort, + }) + if (criteria.sort != null && criteria.sort.direction !== sort.direction) { + updateCaseTable({ + tableType, + updates: { + sort: { + field: sort.field, + direction: criteria.sort.direction as Direction, + }, + }, + }); + } + }, + [tableType, sort.direction, updateCaseTable] + ); + + return ( + + } + headerUnit={'cases'} + id={id} + limit={limit} + loading={loading} + loadPage={page => updateActivePage(page)} + onChange={onChange} + pageOfItems={cases.saved_objects} + showMorePagesIndicator={false} + sorting={sort} + totalCount={cases.total} + updateActivePage={updateActivePage} + updateLimitPagination={updateLimitPagination} + /> + ); + } +); + +CasesPaginatedTableComponent.displayName = 'CasesPaginatedTableComponent'; +const makeMapStateToProps = () => { + const getCasesSelector = caseSelectors.casesSelector(); + return (state: State) => getCasesSelector(state); +}; +export const CasesPaginatedTable = compose>( + connect(makeMapStateToProps, { + updateCaseTable: caseActions.updateCaseTable, + }) +)(CasesPaginatedTableComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx index ad3248406c0478..9f243c5b290275 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx @@ -5,14 +5,22 @@ */ import { getOr } from 'lodash/fp'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; import React from 'react'; import { Query } from 'react-apollo'; -import { GetCasesQuery, CasesSavedObjects, Direction, SortFieldCase } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; +import { + GetCasesQuery, + CasesSavedObjects, + SortCase, +} from '../../../graphql/types'; +import { inputsModel, State } from '../../../store'; import { getDefaultFetchPolicy } from '../../helpers'; import { QueryTemplateProps } from '../../query_template'; import { casesQuery } from './index.gql_query'; +import { caseSelectors } from '../../../store/case'; +import { QueryTemplatePaginated } from '../../query_template_paginated'; const ID = 'casesQuery'; @@ -20,69 +28,90 @@ export interface CasesArgs { id: string; cases: CasesSavedObjects; loading: boolean; + loadPage: (newActivePage: number) => void; refetch: inputsModel.Refetch; } -export interface CasesProps extends QueryTemplateProps { +export interface OwnProps extends QueryTemplateProps { children: (args: CasesArgs) => React.ReactNode; search?: string; } -const CasesComponentQuery = React.memo(({ id = ID, children, skip, search }) => ( - - query={casesQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - pageInfo: { - pageIndex: 1, - pageSize: 5, - }, - search, - sort: { - sortField: SortFieldCase.created_at, - sortOrder: Direction.asc, - }, - }} - > - {({ data, loading, refetch }) => { - const init: CasesSavedObjects = { - page: 0, - per_page: 0, - total: 0, - saved_objects: [ - { - id: '000', - type: '', - updated_at: '', - version: '', - attributes: { - case_type: '', - created_at: 1234235345, - created_by: { - username: '', - full_name: null, - }, - description: '', - state: 'open', - tags: [], - title: '', - }, - }, - ], - }; - const caseData: CasesSavedObjects = getOr(init, 'getCases', data); - return children({ - id: ID, - cases: caseData, - loading, - refetch, - }); - }} - -)); +export interface CasesQueryReduxProps { + activePage: number; + limit: number; + sort: SortCase; +} + +type CasesComponentQueryProps = OwnProps & CasesQueryReduxProps; -CasesComponentQuery.displayName = 'CasesComponentQuery'; +class CasesComponentQuery extends QueryTemplatePaginated< + CasesComponentQueryProps, + GetCasesQuery.Query, + GetCasesQuery.Variables +> { + public render() { + const { activePage, children, limit, search, skip, sort } = this.props; + console.log('Did we update?!?1', this.props); + return ( + + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={casesQuery} + skip={skip} + variables={{ + pageInfo: { + pageIndex: activePage, + pageSize: limit, + }, + search, + sort, + }} + > + {({ data, loading, refetch }) => { + const init: CasesSavedObjects = { + page: 0, + per_page: 0, + total: 0, + saved_objects: [ + { + id: '000', + type: '', + updated_at: '', + version: '', + attributes: { + case_type: '', + created_at: 1234235345, + created_by: { + username: '', + full_name: null, + }, + description: '', + state: 'open', + tags: [], + title: '', + }, + }, + ], + }; + const caseData: CasesSavedObjects = getOr(init, 'getCases', data); + return children({ + id: ID, + cases: caseData, + loading, + loadPage: this.wrappedLoadMore, + refetch, + }); + }} + + ); + } +} -export const CasesQuery = CasesComponentQuery; +const makeMapStateToProps = () => { + const getCasesSelector = caseSelectors.casesSelector(); + return (state: State) => getCasesSelector(state); +}; +export const CasesQuery = compose>(connect(makeMapStateToProps))( + CasesComponentQuery +); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 2bcf11e5cf5b65..a8a30f009f2324 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -588,7 +588,7 @@ "fields": null, "inputFields": [ { - "name": "sortField", + "name": "field", "description": "", "type": { "kind": "NON_NULL", @@ -598,7 +598,7 @@ "defaultValue": null }, { - "name": "sortOrder", + "name": "direction", "description": "", "type": { "kind": "NON_NULL", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 69a5bb03cd38df..466c3340ac5139 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -15,9 +15,9 @@ export interface PageInfoCase { } export interface SortCase { - sortField: SortFieldCase; + field: SortFieldCase; - sortOrder: Direction; + direction: Direction; } export interface PageInfoNote { diff --git a/x-pack/legacy/plugins/siem/public/store/case/actions.ts b/x-pack/legacy/plugins/siem/public/store/case/actions.ts new file mode 100644 index 00000000000000..7a013847299728 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/case/actions.ts @@ -0,0 +1,15 @@ +/* + * 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 actionCreatorFactory from 'typescript-fsa'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/case'); +import { caseModel } from './index'; + +export const updateCaseTable = actionCreator<{ + tableType: caseModel.CaseTableType; + updates: caseModel.TableUpdates; +}>('UPDATE_CASE_TABLE'); diff --git a/x-pack/legacy/plugins/siem/public/store/case/index.ts b/x-pack/legacy/plugins/siem/public/store/case/index.ts new file mode 100644 index 00000000000000..a92c163d978e1a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/case/index.ts @@ -0,0 +1,12 @@ +/* + * 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 * as caseActions from './actions'; +import * as caseModel from './model'; +import * as caseSelectors from './selectors'; + +export { caseActions, caseModel, caseSelectors }; +export * from './reducer'; diff --git a/x-pack/legacy/plugins/siem/public/store/case/model.ts b/x-pack/legacy/plugins/siem/public/store/case/model.ts new file mode 100644 index 00000000000000..a21832cc27610a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/case/model.ts @@ -0,0 +1,37 @@ +/* + * 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 { SortCase } from '../../graphql/types'; + +export enum CaseTableType { + cases = 'cases', +} +export interface TableUpdates { + activePage?: number; + limit?: number; + sort?: SortCase; +} + +export interface BasicQueryPaginated { + activePage: number; + limit: number; +} + +export interface CasesQuery extends BasicQueryPaginated { + sort: SortCase; +} + +export interface CaseQueries { + [CaseTableType.cases]: CasesQuery; +} + +export interface CasePageModel { + queries: CaseQueries; +} + +export interface CaseModel { + page: CasePageModel; +} diff --git a/x-pack/legacy/plugins/siem/public/store/case/reducer.ts b/x-pack/legacy/plugins/siem/public/store/case/reducer.ts new file mode 100644 index 00000000000000..afb2b41db4a9be --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/case/reducer.ts @@ -0,0 +1,44 @@ +/* + * 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 { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { CaseModel, CaseTableType } from './model'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../constants'; +import { Direction, SortFieldCase } from '../../graphql/types'; +import { updateCaseTable } from './actions'; + +export type CaseState = CaseModel; + +export const initialCaseState: CaseState = { + page: { + queries: { + [CaseTableType.cases]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: SortFieldCase.created_at, + direction: Direction.desc, + }, + }, + }, + }, +}; + +export const caseReducer = reducerWithInitialState(initialCaseState) + .case(updateCaseTable, (state, { tableType, updates }) => ({ + ...state, + page: { + ...state.page, + queries: { + ...state.page.queries, + [tableType]: { + ...state.page.queries[tableType], + ...updates, + }, + }, + }, + })) + .build(); diff --git a/x-pack/legacy/plugins/siem/public/store/case/selectors.ts b/x-pack/legacy/plugins/siem/public/store/case/selectors.ts new file mode 100644 index 00000000000000..b22bb44905b936 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/case/selectors.ts @@ -0,0 +1,12 @@ +/* + * 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 { createSelector } from 'reselect'; +import { State } from '../reducer'; +import { CasePageModel } from '../case/model'; + +const selectCasePage = (state: State): CasePageModel => state.case.page; +export const casesSelector = () => + createSelector(selectCasePage, casePage => casePage.queries.cases); diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts index 6f04f22866be5b..99ae3858d92ebc 100644 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/model.ts @@ -5,9 +5,10 @@ */ export { appModel } from './app'; -export { inputsModel } from './inputs'; -export { hostsModel } from './hosts'; +export { caseModel } from './case'; export { dragAndDropModel } from './drag_and_drop'; +export { hostsModel } from './hosts'; +export { inputsModel } from './inputs'; export { networkModel } from './network'; export type KueryFilterQueryKind = 'kuery' | 'lucene'; diff --git a/x-pack/legacy/plugins/siem/public/store/reducer.ts b/x-pack/legacy/plugins/siem/public/store/reducer.ts index 3c93dfa61b7683..035fd5c09467c1 100644 --- a/x-pack/legacy/plugins/siem/public/store/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/reducer.ts @@ -11,11 +11,13 @@ import { dragAndDropReducer, DragAndDropState, initialDragAndDropState } from '. import { hostsReducer, HostsState, initialHostsState } from './hosts'; import { initialInputsState, inputsReducer, InputsState } from './inputs'; import { initialNetworkState, networkReducer, NetworkState } from './network'; +import { initialCaseState, caseReducer, CaseState } from './case'; import { initialTimelineState, timelineReducer } from './timeline/reducer'; import { TimelineState } from './timeline/types'; export interface State { app: AppState; + case: CaseState; dragAndDrop: DragAndDropState; hosts: HostsState; inputs: InputsState; @@ -25,6 +27,7 @@ export interface State { export const initialState: State = { app: initialAppState, + case: initialCaseState, dragAndDrop: initialDragAndDropState, hosts: initialHostsState, inputs: initialInputsState, @@ -34,6 +37,7 @@ export const initialState: State = { export const reducer = combineReducers({ app: appReducer, + case: caseReducer, dragAndDrop: dragAndDropReducer, hosts: hostsReducer, inputs: inputsReducer, diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts index 4cf2782f9b5abe..c32de3e70bee3a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts @@ -17,8 +17,8 @@ export const caseSchema = gql` } input SortCase { - sortField: SortFieldCase! - sortOrder: Direction! + field: SortFieldCase! + direction: Direction! } ############### #### QUERY #### diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index 82974b3a30da0c..978bfaf5e20d28 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -17,9 +17,9 @@ export interface PageInfoCase { } export interface SortCase { - sortField: SortFieldCase; + field: SortFieldCase; - sortOrder: Direction; + direction: Direction; } export interface PageInfoNote { diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts index 13422b69069361..cd3abb3ee44077 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts @@ -31,11 +31,11 @@ export class Case { const options: SavedObjectsFindOptions = { type: caseSavedObjectType, perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, + page: pageInfo != null ? pageInfo.pageIndex + 1 : undefined, // + 1 because table pagination starts at 0 and saved object page index starts at 1 search: search != null ? search : undefined, searchFields: ['tags'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, + sortField: sort != null ? sort.field : undefined, + sortOrder: sort != null ? sort.direction : undefined, }; console.log('getCases return') From 17edc5b026f47b92d7a5717385ab71e1fd0945df Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 16 Jan 2020 13:28:51 -0700 Subject: [PATCH 06/67] sorting by date created --- .../public/components/page/case/cases_table/table.tsx | 8 +++----- .../siem/public/containers/case/get_cases/index.tsx | 7 +------ .../legacy/plugins/siem/server/lib/case/saved_object.ts | 5 +---- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx index 00e5a79efee38a..73520e8ca4b889 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx @@ -60,10 +60,6 @@ export const CasesPaginatedTableComponent = React.memo( const onChange = useCallback( (criteria: Criteria) => { - console.log('SORT', { - criteria: criteria.sort, - sort, - }) if (criteria.sort != null && criteria.sort.direction !== sort.direction) { updateCaseTable({ tableType, @@ -79,6 +75,8 @@ export const CasesPaginatedTableComponent = React.memo( [tableType, sort.direction, updateCaseTable] ); + const sorting = { field: `attributes.${sort.field}`, direction: sort.direction }; + return ( { public render() { const { activePage, children, limit, search, skip, sort } = this.props; - console.log('Did we update?!?1', this.props); return ( fetchPolicy={getDefaultFetchPolicy()} diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts index cd3abb3ee44077..9176445783051b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts @@ -38,14 +38,13 @@ export class Case { sortOrder: sort != null ? sort.direction : undefined, }; - console.log('getCases return') return this.getAllSavedCase(request, options); } private async getSavedCase(request: FrameworkRequest, caseId: string) { const savedObjectsClient = request.context.core.savedObjects.client; const savedObject = await savedObjectsClient.get(caseSavedObjectType, caseId); - // console.log( + // // 'SAVED CASE!!!', // JSON.stringify({ // caseSavedObjectType, @@ -58,10 +57,8 @@ export class Case { } private async getAllSavedCase(request: FrameworkRequest, options: SavedObjectsFindOptions) { - console.log('getAllSavedCase start', options) const savedObjectsClient = request.context.core.savedObjects.client; const savedObjects = await savedObjectsClient.find(options); - console.log('getAllSavedCase return') return { ...savedObjects, saved_objects: savedObjects.saved_objects.map(savedObject => From fe70b50c6c203b5ea0cd72aeeda825ae5c3c6105 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 20 Jan 2020 09:08:00 -0700 Subject: [PATCH 07/67] trying to get routing working --- .../siem/public/components/link_to/index.ts | 2 +- .../public/components/link_to/link_to.tsx | 10 ++- .../components/link_to/redirect_to_case.tsx | 17 +++-- .../siem/public/components/links/index.tsx | 13 +++- .../public/components/navigation/index.tsx | 1 + .../navigation/tab_navigation/index.tsx | 1 - .../components/page/case/case_view/index.tsx | 7 +- .../page/case/cases_table/columns.tsx | 8 ++- .../public/components/url_state/constants.ts | 8 ++- .../public/components/url_state/helpers.ts | 7 ++ .../siem/public/components/url_state/types.ts | 2 + .../public/containers/case/get_case/index.tsx | 2 +- .../plugins/siem/public/mock/global_state.ts | 15 ++++ .../siem/public/pages/case/case.test.tsx | 4 +- .../plugins/siem/public/pages/case/case.tsx | 7 +- .../siem/public/pages/case/case_details.tsx | 69 +++++++++++++++++++ .../plugins/siem/public/pages/case/index.tsx | 27 +++++++- .../plugins/siem/public/pages/home/index.tsx | 5 +- 18 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index e03437f639cdc2..85bf9a128652f8 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -13,4 +13,4 @@ export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines'; -export { getCaseUrl, RedirectToCasePage } from './redirect_to_case'; +export { getCaseUrl, getCaseDetailsUrl, RedirectToCasePage } from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 93974010741b68..79afcf9361023b 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -32,7 +32,15 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToOverviewPage} path={`${match.url}/:pageName(${SiemPageName.overview})`} /> - + + ; -export const RedirectToCasePage = ({ location: { search } }: CaseComponentProps) => ( - +export const RedirectToCasePage = ({ + match: { + params: { detailName }, + }, +}: CaseComponentProps) => ( + ); -export const getCaseUrl = () => `#/link-to/${SiemPageName.case}`; +const baseCaseUrl = `#/link-to/${SiemPageName.case}`; + +export const getCaseUrl = () => baseCaseUrl; +export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index e122b3e235a9e5..66148b5bf8b05b 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -8,7 +8,7 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { encodeIpv6 } from '../../lib/helpers'; -import { getHostDetailsUrl, getIPDetailsUrl } from '../link_to'; +import { getCaseDetailsUrl, getHostDetailsUrl, getIPDetailsUrl } from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; // Internal Links @@ -35,6 +35,17 @@ const IPDetailsLinkComponent: React.FC<{ export const IPDetailsLink = React.memo(IPDetailsLinkComponent); +const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string }> = ({ + children, + detailName, +}) => ( + + {children ? children : detailName} + +); +export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); +CaseDetailsLink.displayName = 'CaseDetailsLink'; + // External Links export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index 61ac84667d80f7..e1b1bad7a8f91e 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -21,6 +21,7 @@ export const SiemNavigationComponent = React.memo< >( ({ detailName, display, navTabs, pageName, pathName, search, tabName, urlState, flowTarget }) => { useEffect(() => { + console.log('pathName', pathName) if (pathName) { setBreadcrumbs({ query: urlState.query, diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index b653624ec1f67f..aa7c224e6c4f9e 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -54,7 +54,6 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab()); useEffect(() => { const currentTabSelected = mapLocationToTab(); - if (currentTabSelected !== selectedTabId) { setSelectedTabId(currentTabSelected); } diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx index a14488090bd1e1..687229e45cdc11 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx @@ -10,9 +10,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { CaseQuery } from '../../../../containers/case/get_case'; -export const CaseView = React.memo(() => ( +interface Props { + caseId: string; +} +export const CaseView = React.memo(({ caseId }: Props) => ( - + {children => (

{children.case.attributes.title}

diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx index 7ccd5c3dfb0e46..a835c5e3cb9024 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx @@ -8,6 +8,7 @@ import { getEmptyTagValue } from '../../../empty_value'; import { Columns } from '../../../paginated_table'; import { CaseSavedObject } from '../../../../graphql/types'; import { FormattedRelativePreferenceDate } from '../../../formatted_date'; +import { CaseDetailsLink } from '../../../links'; export type CasesColumns = [ Columns, @@ -29,7 +30,12 @@ export const getCasesColumns = (): CasesColumns => [ { field: 'id', name: 'Case Id', - render: id => renderStringField(id), + render: id => { + if (id != null) { + return ; + } + return getEmptyTagValue(); + }, }, { field: 'attributes.created_at', diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts index 6468d1299832bf..2b196564beeea0 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts @@ -6,6 +6,8 @@ export enum CONSTANTS { appQuery = 'query', + caseDetails = 'case.details', + casePage = 'case.page', detectionEnginePage = 'detectionEngine.page', filters = 'filters', hostsDetails = 'hosts.details', @@ -14,16 +16,16 @@ export enum CONSTANTS { networkPage = 'network.page', overviewPage = 'overview.page', savedQuery = 'savedQuery', + timeline = 'timeline', timelinePage = 'timeline.page', timerange = 'timerange', - timeline = 'timeline', unknown = 'unknown', } export type UrlStateType = + | 'case' | 'detection-engine' | 'host' | 'network' | 'overview' - | 'timeline' - | 'case'; + | 'timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index aa340b54c1699f..16ca9592a81ccd 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -82,6 +82,8 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'detection-engine'; } else if (pageName === SiemPageName.timelines) { return 'timeline'; + } else if (pageName === SiemPageName.case) { + return 'case'; } return 'overview'; }; @@ -115,6 +117,11 @@ export const getCurrentLocation = ( return CONSTANTS.detectionEnginePage; } else if (pageName === SiemPageName.timelines) { return CONSTANTS.timelinePage; + } else if (pageName === SiemPageName.case) { + if (detailName != null) { + return CONSTANTS.caseDetails; + } + return CONSTANTS.casePage; } return CONSTANTS.unknown; }; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index 1509b5b17bc6df..f7ac6b8bdb2f1b 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -57,6 +57,8 @@ export const URL_STATE_KEYS: Record = { }; export type LocationTypes = + | CONSTANTS.caseDetails + | CONSTANTS.casePage | CONSTANTS.detectionEnginePage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx index 4033c07497ee5f..33f7bc6842f4f1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx @@ -24,8 +24,8 @@ export interface CaseArgs { } export interface CaseProps extends QueryTemplateProps { - caseId: string; children: (args: CaseArgs) => React.ReactNode; + caseId: string; } const CaseComponentQuery = React.memo(({ children, skip, caseId }) => ( diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index 31e203d0803222..7450b82f99d759 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -11,6 +11,7 @@ import { HostsFields, NetworkDnsFields, NetworkTopTablesFields, + SortFieldCase, TlsFields, UsersFields, } from '../graphql/types'; @@ -32,6 +33,20 @@ export const mockGlobalState: State = { { id: 'error-id-2', title: 'title-2', message: ['error-message-2'] }, ], }, + case: { + page: { + queries: { + cases: { + activePage: 0, + limit: 10, + sort: { + direction: Direction.desc, + field: SortFieldCase.created_at, + }, + }, + }, + }, + }, hosts: { page: { queries: { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx index 61ef36a58fc2df..770b7342722fec 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.test.tsx @@ -39,7 +39,7 @@ describe('Case', () => { - + @@ -56,7 +56,7 @@ describe('Case', () => { - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 6ba310081b5258..8c617237d792f4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -13,9 +13,8 @@ import { EmptyPage } from '../../components/empty_page'; import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { SpyRoute } from '../../utils/route/spy_routes'; import { CasesTable } from '../../components/page/case/cases_table'; -import { CaseView } from '../../components/page/case/case_view'; +import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; const basePath = chrome.getBasePath(); @@ -41,7 +40,6 @@ export const CaseComponent = React.memo(() => { {({ indicesExist }) => indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - ) : ( @@ -52,7 +50,7 @@ export const CaseComponent = React.memo(() => { actionSecondaryIcon="popout" actionSecondaryLabel={i18n.EMPTY_ACTION_SECONDARY} actionSecondaryTarget="_blank" - actionSecondaryUrl={docLinks.links.siem} + actionSecondaryUrl={docLinks.links.siem.gettingStarted} data-test-subj="empty-page" title={i18n.EMPTY_TITLE} /> @@ -60,7 +58,6 @@ export const CaseComponent = React.memo(() => { } - ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx new file mode 100644 index 00000000000000..8bb10684601d82 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import React from 'react'; +import chrome from 'ui/chrome'; + +import { useKibana } from '../../lib/kibana'; +import { EmptyPage } from '../../components/empty_page'; +import { HeaderPage } from '../../components/header_page'; +import { WrapperPage } from '../../components/wrapper_page'; +import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; +import { CaseView } from '../../components/page/case/case_view'; +import * as i18n from './translations'; +import { SpyRoute } from '../../utils/route/spy_routes'; + +const basePath = chrome.getBasePath(); + +interface Props { + caseId: string; +} + +export const CaseDetails = React.memo(({ caseId }: Props) => { + const docLinks = useKibana().services.docLinks; + return ( + <> + + + + + {({ indicesExist }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + ) : ( + + ) + } + + + + + ); +}); + +CaseDetails.displayName = 'CaseDetails'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx index 552f16c1060a7b..d7888fad72d2a9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -4,10 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React from 'react'; +import { Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { SiemPageName } from '../home/types'; import { CaseComponent } from './case'; +import { CaseDetails } from './case_details'; -export const Case = memo(() => ); +type Props = Partial> & { url: string }; +const casesPagePath = `/:pageName(${SiemPageName.case})`; +const caseDetailsPagePath = `${casesPagePath}/:detailName`; + +const CaseContainerComponent: React.FC = () => { + return ( + + } /> + } + /> + + ); +}; + +export const Case = React.memo(CaseContainerComponent); Case.displayName = 'Case'; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index f95436d52abfa0..787f41af44c4a9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -128,7 +128,10 @@ export const HomePage: React.FC = () => ( )} /> - } /> + } + /> } /> From 519c424c1aec5bbb6f31d2805ce5f4ae461125df Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 20 Jan 2020 09:55:40 -0700 Subject: [PATCH 08/67] routing fixed --- .../navigation/breadcrumbs/index.ts | 12 ++++++++ .../public/components/navigation/index.tsx | 1 - .../plugins/siem/public/pages/case/index.tsx | 3 +- .../plugins/siem/public/pages/case/utils.ts | 29 +++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 9eee5b21e83f3c..8e4c04e2ed411e 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -10,6 +10,7 @@ import { getOr, omit } from 'lodash/fp'; import { APP_NAME } from '../../../../common/constants'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; +import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; import { SiemPageName } from '../../../pages/home/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; import { getOverviewUrl } from '../../link_to'; @@ -38,6 +39,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => spyState != null && spyState.pageName === SiemPageName.hosts; +const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => + spyState != null && spyState.pageName === SiemPageName.case; + export const getBreadcrumbsForRoute = ( object: RouteSpyState & TabNavigationProps ): Breadcrumb[] | null => { @@ -76,6 +80,14 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isCaseRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + return [...siemRootBreadcrumb, ...getCaseDetailsBreadcrumbs(spyState)]; + } if ( spyState != null && object.navTabs && diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index e1b1bad7a8f91e..61ac84667d80f7 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -21,7 +21,6 @@ export const SiemNavigationComponent = React.memo< >( ({ detailName, display, navTabs, pageName, pathName, search, tabName, urlState, flowTarget }) => { useEffect(() => { - console.log('pathName', pathName) if (pathName) { setBreadcrumbs({ query: urlState.query, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx index d7888fad72d2a9..28e44cc06cbb5c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -19,8 +19,9 @@ const caseDetailsPagePath = `${casesPagePath}/:detailName`; const CaseContainerComponent: React.FC = () => { return ( - } /> + } /> { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getCaseUrl(), + }, + ]; + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: getCaseDetailsUrl(params.detailName), + }, + ]; + } + return breadcrumb; +}; From 43da47767174cad3f125ff5e890eccc1d7cb2492 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 20 Jan 2020 10:31:18 -0700 Subject: [PATCH 09/67] fixing --- .../components/page/case/case_view/index.tsx | 103 +++++++++++------- .../siem/public/pages/case/case_details.tsx | 52 ++++----- .../siem/public/pages/case/translations.ts | 4 + 3 files changed, 87 insertions(+), 72 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx index 687229e45cdc11..2d5816075929db 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx @@ -5,10 +5,13 @@ */ import React from 'react'; -import { EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CaseQuery } from '../../../../containers/case/get_case'; +import { HeaderPage } from '../../../header_page'; +import * as i18n from '../../../../pages/case/translations'; +import { getCaseUrl } from '../../../link_to'; interface Props { caseId: string; @@ -17,45 +20,65 @@ export const CaseView = React.memo(({ caseId }: Props) => ( {children => ( - -

{children.case.attributes.title}

-
-
- -
-
{children.case.attributes.description}
-
- -
-
{children.case.attributes.case_type}
-
- -
-
{children.case.attributes.state}
-
- -
-
{children.case.updated_at}
-
- -
-
{children.case.attributes.created_at}
-
- -
-
{children.case.attributes.created_by.username}
-
- -
-
-
    - {children.case.attributes.tags.map((tag, key) => ( -
  • {tag}
  • - ))} -
-
-
-
+ <> + + {i18n.BACK_LABEL} + + + +
+
+ +
+
{children.case.attributes.description}
+
+ +
+
{children.case.attributes.case_type}
+
+ +
+
{children.case.attributes.state}
+
+ +
+
{children.case.updated_at}
+
+ +
+
{children.case.attributes.created_at}
+
+ +
+
{children.case.attributes.created_by.username}
+
+ +
+
+
    + {children.case.attributes.tags.map((tag, key) => ( +
  • {tag}
  • + ))} +
+
+
+
+ )}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index 8bb10684601d82..1123cc7f8860fb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -10,7 +10,6 @@ import chrome from 'ui/chrome'; import { useKibana } from '../../lib/kibana'; import { EmptyPage } from '../../components/empty_page'; -import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; import { CaseView } from '../../components/page/case/case_view'; @@ -27,40 +26,29 @@ export const CaseDetails = React.memo(({ caseId }: Props) => { const docLinks = useKibana().services.docLinks; return ( <> - - - - - {({ indicesExist }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + {({ indicesExist }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + - ) : ( - - ) - } - - + + ) : ( + + ) + } + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 277433518f084d..480518e436800d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -23,6 +23,10 @@ export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.pageBadgeToolt 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', }); +export const BACK_LABEL = i18n.translate('xpack.siem.case.pageBackLabel', { + defaultMessage: '< Back to all cases', +}); + export const EMPTY_TITLE = i18n.translate('xpack.siem.case.emptyTitle', { defaultMessage: 'It looks like you don’t have any indices relevant to the SIEM application', }); From f31c2543fcb3417869c03a5007aec87b0d977826 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 21 Jan 2020 09:45:58 -0700 Subject: [PATCH 10/67] using saved object api, switching --- .../page/case/cases_table/columns.tsx | 2 ++ .../page/case/cases_table/index.tsx | 15 +++++-------- .../page/case/cases_table/table.tsx | 22 +++++++++++++++++-- .../components/paginated_table/index.tsx | 4 +++- .../siem/public/graphql/introspection.json | 7 ++++++ .../plugins/siem/public/graphql/types.ts | 2 ++ .../siem/server/graphql/case/schema.gql.ts | 17 ++------------ .../plugins/siem/server/graphql/types.ts | 2 ++ .../siem/server/lib/case/saved_object.ts | 1 + 9 files changed, 44 insertions(+), 28 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx index a835c5e3cb9024..13ab08b8bf1002 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx @@ -56,6 +56,7 @@ export const getCasesColumns = (): CasesColumns => [ { field: 'updated_at', name: 'Last updated', + sortable: true, render: updatedAt => { if (updatedAt != null) { return ; @@ -66,6 +67,7 @@ export const getCasesColumns = (): CasesColumns => [ { field: 'attributes.state', name: 'State', + sortable: true, render: state => renderStringField(state), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx index 3b9e83d85e4135..48ce34ace3f45b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx @@ -10,15 +10,10 @@ import { EuiFlexItem } from '@elastic/eui'; import { CasesQuery } from '../../../../containers/case/get_cases'; import { CasesPaginatedTable } from './table'; -export const CasesTable = React.memo(() => { - - return ( - - - {children => } - - - ); -}); +export const CasesTable = React.memo(() => ( + + {children => } + +)); CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx index 73520e8ca4b889..ed9326077a92d9 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx @@ -12,7 +12,7 @@ import { ActionCreator } from 'typescript-fsa'; import { Criteria, PaginatedTable } from '../../../paginated_table'; import { getCasesColumns } from './columns'; -import { CasesSavedObjects, Direction, SortCase } from '../../../../graphql/types'; +import { CasesSavedObjects, Direction, SortCase, SortFieldCase } from '../../../../graphql/types'; import { caseActions, caseSelectors, caseModel } from '../../../../store/case'; import { State } from '../../../../store'; @@ -60,12 +60,29 @@ export const CasesPaginatedTableComponent = React.memo( const onChange = useCallback( (criteria: Criteria) => { + console.log('Critera sort', criteria.sort); + console.log('sortsortsort', sort); if (criteria.sort != null && criteria.sort.direction !== sort.direction) { + let newSort; + switch (criteria.sort.field) { + case 'attributes.state': + newSort = SortFieldCase.state; + break; + case 'attributes.created_at': + newSort = SortFieldCase.created_at; + break; + case 'updated_at': + newSort = SortFieldCase.updated_at; + break; + default: + newSort = SortFieldCase.created_at; + } + console.log('newSort', newSort) updateCaseTable({ tableType, updates: { sort: { - field: sort.field, + field: newSort, direction: criteria.sort.direction as Direction, }, }, @@ -86,6 +103,7 @@ export const CasesPaginatedTableComponent = React.memo( } headerUnit={'cases'} + hideInspect={true} id={id} limit={limit} loading={loading} diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index 21199205aa59ca..d19b49ed2b5d3c 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -98,6 +98,7 @@ export interface BasicTableProps { headerTitle: string | React.ReactElement; headerTooltip?: string; headerUnit: string | React.ReactElement; + hideInspect?: boolean; id?: string; itemsPerRow?: ItemsPerRow[]; isInspect?: boolean; @@ -136,6 +137,7 @@ const PaginatedTableComponent: FC = ({ headerTitle, headerTooltip, headerUnit, + hideInspect, id, isInspect, itemsPerRow, @@ -230,7 +232,7 @@ const PaginatedTableComponent: FC = ({ const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; return ( - + Date: Tue, 21 Jan 2020 16:46:49 -0700 Subject: [PATCH 11/67] using hooks, need to fix perPage --- .../page/case/cases_table/index.tsx | 16 +-- .../page/case/cases_table/table_hook.tsx | 128 +++++++++++++++++ .../page/case/cases_table/translations.ts | 27 ++++ .../siem/public/components/page/case/types.ts | 65 +++++++++ .../siem/public/hooks/case/constants.ts | 10 ++ .../siem/public/hooks/case/use_case_api.tsx | 136 ++++++++++++++++++ .../siem/server/lib/case/saved_object.ts | 19 ++- .../case/server/routes/api/get_all_cases.ts | 18 ++- .../plugins/case/server/routes/api/schema.ts | 12 ++ .../plugins/case/server/routes/api/types.ts | 2 + x-pack/plugins/case/server/services/index.ts | 11 +- 11 files changed, 425 insertions(+), 19 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/cases_table/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/hooks/case/constants.ts create mode 100644 x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx index 48ce34ace3f45b..1facfc1c4541c6 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx @@ -6,14 +6,14 @@ import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; +import { CasesPaginatedTable } from './table_hook'; -import { CasesQuery } from '../../../../containers/case/get_cases'; -import { CasesPaginatedTable } from './table'; - -export const CasesTable = React.memo(() => ( - - {children => } - -)); +export const CasesTable = React.memo(() => { + return ( + + + + ); +}); CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx new file mode 100644 index 00000000000000..63eb17d1e0fdf6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx @@ -0,0 +1,128 @@ +/* + * 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, { useCallback } from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionCreator } from 'typescript-fsa'; +import * as i18n from './translations'; +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; + +import { getCasesColumns } from './columns'; +import { Direction, SortFieldCase } from '../../../../graphql/types'; +import { caseActions, caseModel } from '../../../../store/case'; +import { useCaseApi } from '../../../../hooks/case/use_case_api'; + +interface CasesTableProps { + id: string; +} + +interface CasesTableDispatchProps { + updateCaseTable: ActionCreator<{ + tableType: caseModel.CaseTableType; + updates: caseModel.TableUpdates; + }>; +} + +type CasesPaginatedTableProps = CasesTableProps & CasesTableDispatchProps; +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; +export const CasesPaginatedTableComponent = React.memo( + ({ id, updateCaseTable }: CasesPaginatedTableProps) => { + const [ + { + data, + isLoading, + isError, + table: { page, perPage, sortOrder, sortField }, + }, + doFetch, + ] = useCaseApi(); + + const updateActivePage = newPage => { + console.log('updateActivePage newPage', newPage); + return doFetch({ + page: newPage + 1, + }); + }; + + const updateLimitPagination = newLimit => { + console.log('updateLimitPagination newLimit', newLimit); + return doFetch({ + perPage: newLimit, + }); + }; + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null && criteria.sort.direction !== sortOrder) { + let newSort; + switch (criteria.sort.field) { + case 'attributes.state': + newSort = SortFieldCase.state; + break; + case 'attributes.created_at': + newSort = SortFieldCase.created_at; + break; + case 'updated_at': + newSort = SortFieldCase.updated_at; + break; + default: + newSort = SortFieldCase.created_at; + } + doFetch({ + sortField: newSort, + sortOrder: criteria.sort.direction as Direction, + }); + } + }, + [sortOrder, updateCaseTable] + ); + + const sorting = { field: `attributes.${sortField}`, direction: sortOrder }; + return isError ? null : ( + + } + headerUnit={i18n.UNIT(data.total)} + hideInspect={true} + id={id} + itemsPerRow={rowItems} + limit={perPage} + loading={isLoading} + loadPage={newPage => updateActivePage(newPage)} + onChange={onChange} + pageOfItems={data.saved_objects} + showMorePagesIndicator={false} + sorting={sorting} + totalCount={data.total} + updateActivePage={updateActivePage} + updateLimitPagination={updateLimitPagination} + /> + ); + } +); + +CasesPaginatedTableComponent.displayName = 'CasesPaginatedTableComponent'; + +export const CasesPaginatedTable = compose>( + connect(null, { + updateCaseTable: caseActions.updateCaseTable, + }) +)(CasesPaginatedTableComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/translations.ts new file mode 100644 index 00000000000000..7d5c8bc1b8f7a6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/translations.ts @@ -0,0 +1,27 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ALL_CASES = i18n.translate('xpack.siem.caseTable.title', { + defaultMessage: 'All Cases', +}); + +export const ROWS_5 = i18n.translate('xpack.siem.caseTable.rowsFive', { + values: { numRows: 5 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); + +export const ROWS_10 = i18n.translate('xpack.siem.caseTable.rowsTen', { + values: { numRows: 10 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.caseTable.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, + }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/types.ts b/x-pack/legacy/plugins/siem/public/components/page/case/types.ts new file mode 100644 index 00000000000000..1d0c38c5c29b3f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/case/types.ts @@ -0,0 +1,65 @@ +/* + * 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 { ElasticUser, Maybe } from '../../../graphql/types'; +import { SavedObjectsBaseOptions } from 'kibana/server'; + +export interface CasesSavedObjects { + saved_objects: Array>; + + page: number; + + per_page: number; + + total: number; +} + +export interface CaseSavedObject { + attributes: CaseResult; + + id: string; + + type: string; + + updated_at: string; + + version: string; +} +export interface CaseResult { + case_type: string; + + created_at: number; + + created_by: ElasticUser; + + description: string; + + state: string; + + tags: Array>; + + title: string; +} + +export interface CaseFindOptions extends SavedObjectsBaseOptions { + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + /** + * An array of fields to include in the results + * @example + * SavedObjects.find({type: 'dashboard', fields: ['attributes.name', 'attributes.location']}) + */ + fields?: string[]; + /** Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information */ + search?: string; + /** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */ + searchFields?: string[]; + hasReference?: { type: string; id: string }; + defaultSearchOperator?: 'AND' | 'OR'; + filter?: string; +} diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/constants.ts b/x-pack/legacy/plugins/siem/public/hooks/case/constants.ts new file mode 100644 index 00000000000000..f9e6037a9efb6c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/hooks/case/constants.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export const FETCH_INIT = 'FETCH_INIT'; +export const FETCH_SUCCESS = 'FETCH_SUCCESS'; +export const FETCH_FAILURE = 'FETCH_FAILURE'; +export const UPDATE_TABLE = 'UPDATE_TABLE'; diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx b/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx new file mode 100644 index 00000000000000..4c590a60198f10 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx @@ -0,0 +1,136 @@ +/* + * 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 { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; + +import chrome from 'ui/chrome'; +import { CasesSavedObjects } from '../../components/page/case/types'; +import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS, UPDATE_TABLE } from './constants'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; +import { Direction, SortFieldCase } from '../../graphql/types'; + +interface TableArgs { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: Direction; +} + +interface QueryArgs { + page?: number; + perPage?: number; + sortField?: SortFieldCase; + sortOrder?: Direction; +} + +interface CasesState { + data: CasesSavedObjects; + isLoading: boolean; + isError: boolean; + table: TableArgs; +} +interface PayloadObj { + [key: string]: unknown; +} +interface Action { + type: string; + payload: CasesSavedObjects | QueryArgs | PayloadObj; +} + +const dataFetchReducer = (state: CasesState, action: Action): CasesState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: action.payload, + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + case UPDATE_TABLE: + console.log('UPDATE_TABLE return'); + return { + ...state, + table: { + ...state.table, + ...action.payload, + }, + }; + default: + throw new Error(); + } +}; +const initialData: CasesSavedObjects = { + page: 0, + per_page: 0, + total: 0, + saved_objects: [], +}; +export const useCaseApi = (): [CasesState, Dispatch>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + table: { + page: DEFAULT_TABLE_ACTIVE_PAGE + 1, + perPage: DEFAULT_TABLE_LIMIT, + sortField: SortFieldCase.created_at, + sortOrder: Direction.desc, + }, + }); + const [query, setQuery] = useState(state.table as QueryArgs); + + useEffect(() => { + console.log('useEffect ONE', { query, table: state.table }); + dispatch({ type: UPDATE_TABLE, payload: query }); + }, [query]); + + useEffect(() => { + console.log('useEffect TWO', { query, table: state.table }); + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT, payload: {} }); + try { + const queryParams = Object.entries(state.table).reduce((acc, [key, value]) => { + return `${acc}${key}=${value}&`; + }, '?'); + const result = await fetch(`${chrome.getBasePath()}/api/cases${queryParams}`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', + }, + }); + if (!didCancel) { + dispatch({ type: FETCH_SUCCESS, payload: await result.json() }); + } + } catch (error) { + console.log('ERRROR', error); + if (!didCancel) { + dispatch({ type: FETCH_FAILURE, payload: {} }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, [state.table]); + return [state, setQuery]; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts index 1e2cffc0eda8e8..dafbccae09c0e1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import axios from 'axios'; import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; import { CaseSavedObject, CasesSavedObjects, PageInfoCase, SortCase } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; @@ -57,12 +58,22 @@ export class Case { } private async getAllSavedCase(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const savedObjectsClient = request.context.core.savedObjects.client; + // const savedObjectsClient = request.context.core.savedObjects.client; console.log('saved objects find options', options); - const savedObjects = await savedObjectsClient.find(options); + let response; + try { + response = await axios.get('/api/cases', { + params: options, + }); + console.log(response); + } catch (error) { + console.error(error); + return error; + } + const { data }: { data: CasesSavedObjects } = response; // await savedObjectsClient.find(options); return { - ...savedObjects, - saved_objects: savedObjects.saved_objects.map(savedObject => + ...data, + saved_objects: data.saved_objects.map(savedObject => convertSavedObjectToSavedCase(savedObject) ), }; diff --git a/x-pack/plugins/case/server/routes/api/get_all_cases.ts b/x-pack/plugins/case/server/routes/api/get_all_cases.ts index 749a183dfe9807..09c81d63dc791b 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_cases.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_cases.ts @@ -4,20 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; import { wrapError } from './utils'; +import { SavedOptionsFindOptionsSchema } from './schema'; export function initGetAllCasesApi({ caseService, router }: RouteDeps) { router.get( { path: '/api/cases', - validate: false, + validate: { + query: schema.nullable(SavedOptionsFindOptionsSchema), + }, }, async (context, request, response) => { try { - const cases = await caseService.getAllCases({ - client: context.core.savedObjects.client, - }); + const args = request.query + ? { + client: context.core.savedObjects.client, + options: request.query, + } + : { + client: context.core.savedObjects.client, + }; + const cases = await caseService.getAllCases(args); return response.ok({ body: cases }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 4a4a0c3a11e362..30222c163c8624 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -42,3 +42,15 @@ export const UpdatedCaseSchema = schema.object({ tags: schema.maybe(schema.arrayOf(schema.string())), case_type: schema.maybe(schema.string()), }); + +export const SavedOptionsFindOptionsSchema = schema.object({ + defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), + fields: schema.maybe(schema.arrayOf(schema.string())), + filter: schema.maybe(schema.string()), + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + search: schema.maybe(schema.string()), + searchFields: schema.maybe(schema.arrayOf(schema.string())), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), +}); diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index d943e4e5fd7dd7..7fe06e9ea1058a 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -9,6 +9,7 @@ import { CommentSchema, NewCaseSchema, NewCommentSchema, + SavedOptionsFindOptionsSchema, UpdatedCaseSchema, UpdatedCommentSchema, UserSchema, @@ -17,6 +18,7 @@ import { export type NewCaseType = TypeOf; export type NewCommentFormatted = TypeOf; export type NewCommentType = TypeOf; +export type SavedOptionsFindOptionsType = TypeOf; export type UpdatedCaseTyped = TypeOf; export type UpdatedCommentType = TypeOf; export type UserType = TypeOf; diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 684d905a5c71fe..a3f218670afe2d 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -18,6 +18,7 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../constants'; import { NewCaseFormatted, NewCommentFormatted, + SavedOptionsFindOptionsType, UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; @@ -33,6 +34,10 @@ interface ClientArgs { interface GetCaseArgs extends ClientArgs { caseId: string; } + +interface GetCasesArgs extends ClientArgs { + options?: SavedOptionsFindOptionsType; +} interface GetCommentArgs extends ClientArgs { commentId: string; } @@ -64,7 +69,7 @@ interface CaseServiceDeps { export interface CaseServiceSetup { deleteCase(args: GetCaseArgs): Promise<{}>; deleteComment(args: GetCommentArgs): Promise<{}>; - getAllCases(args: ClientArgs): Promise; + getAllCases(args: GetCasesArgs): Promise; getAllCaseComments(args: GetCaseArgs): Promise; getCase(args: GetCaseArgs): Promise; getComment(args: GetCommentArgs): Promise; @@ -114,10 +119,10 @@ export class CaseService { throw error; } }, - getAllCases: async ({ client }: ClientArgs) => { + getAllCases: async ({ client, options }: GetCasesArgs) => { try { this.log.debug(`Attempting to GET all cases`); - return await client.find({ type: CASE_SAVED_OBJECT }); + return await client.find({ ...options, type: CASE_SAVED_OBJECT }); } catch (error) { this.log.debug(`Error on GET cases: ${error}`); throw error; From fdb0f1b77152f604ecbd7f8aecd5de9ac2ce8f99 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 22 Jan 2020 07:32:51 -0700 Subject: [PATCH 12/67] fixed per page --- .../page/case/cases_table/table_hook.tsx | 61 +++++++++---------- .../components/paginated_table/index.tsx | 6 +- .../siem/public/hooks/case/use_case_api.tsx | 7 +-- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx index 63eb17d1e0fdf6..1b02696b1dfefb 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx @@ -51,45 +51,39 @@ export const CasesPaginatedTableComponent = React.memo( doFetch, ] = useCaseApi(); - const updateActivePage = newPage => { - console.log('updateActivePage newPage', newPage); - return doFetch({ + const updateActivePage = (newPage: number) => + doFetch({ page: newPage + 1, }); - }; - const updateLimitPagination = newLimit => { - console.log('updateLimitPagination newLimit', newLimit); - return doFetch({ + const updateLimitPagination = (newLimit: number) => + doFetch({ + page: 1, // reset to first page perPage: newLimit, }); - }; - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null && criteria.sort.direction !== sortOrder) { - let newSort; - switch (criteria.sort.field) { - case 'attributes.state': - newSort = SortFieldCase.state; - break; - case 'attributes.created_at': - newSort = SortFieldCase.created_at; - break; - case 'updated_at': - newSort = SortFieldCase.updated_at; - break; - default: - newSort = SortFieldCase.created_at; - } - doFetch({ - sortField: newSort, - sortOrder: criteria.sort.direction as Direction, - }); + const onChange = (criteria: Criteria) => { + if (criteria.sort != null && criteria.sort.direction !== sortOrder) { + let newSort; + switch (criteria.sort.field) { + case 'attributes.state': + newSort = SortFieldCase.state; + break; + case 'attributes.created_at': + newSort = SortFieldCase.created_at; + break; + case 'updated_at': + newSort = SortFieldCase.updated_at; + break; + default: + newSort = SortFieldCase.created_at; } - }, - [sortOrder, updateCaseTable] - ); + doFetch({ + sortField: newSort, + sortOrder: criteria.sort.direction as Direction, + }); + } + }; const sorting = { field: `attributes.${sortField}`, direction: sortOrder }; return isError ? null : ( @@ -105,8 +99,9 @@ export const CasesPaginatedTableComponent = React.memo( id={id} itemsPerRow={rowItems} limit={perPage} + limitResetsActivePage={false} loading={isLoading} - loadPage={newPage => updateActivePage(newPage)} + loadPage={newPage => newPage} onChange={onChange} pageOfItems={data.saved_objects} showMorePagesIndicator={false} diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index d19b49ed2b5d3c..fb17ba409ff516 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -103,6 +103,7 @@ export interface BasicTableProps { itemsPerRow?: ItemsPerRow[]; isInspect?: boolean; limit: number; + limitResetsActivePage?: boolean; loading: boolean; loadPage: (activePage: number) => void; onChange?: (criteria: Criteria) => void; @@ -142,6 +143,7 @@ const PaginatedTableComponent: FC = ({ isInspect, itemsPerRow, limit, + limitResetsActivePage = true, loading, loadPage, onChange = noop, @@ -223,7 +225,9 @@ const PaginatedTableComponent: FC = ({ onClick={() => { closePopover(); updateLimitPagination(item.numberOfRow); - updateActivePage(0); // reset results to first page + if (limitResetsActivePage) { + updateActivePage(0); + } // reset results to first page }} > {item.text} diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx b/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx index 4c590a60198f10..6b69c67cf48259 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx @@ -49,11 +49,12 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { isError: false, }; case FETCH_SUCCESS: + const getSavedObject = a => a as CasesSavedObjects; return { ...state, isLoading: false, isError: false, - data: action.payload, + data: getSavedObject(action.payload), }; case FETCH_FAILURE: return { @@ -62,7 +63,6 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { isError: true, }; case UPDATE_TABLE: - console.log('UPDATE_TABLE return'); return { ...state, table: { @@ -95,12 +95,10 @@ export const useCaseApi = (): [CasesState, Dispatch>] const [query, setQuery] = useState(state.table as QueryArgs); useEffect(() => { - console.log('useEffect ONE', { query, table: state.table }); dispatch({ type: UPDATE_TABLE, payload: query }); }, [query]); useEffect(() => { - console.log('useEffect TWO', { query, table: state.table }); let didCancel = false; const fetchData = async () => { dispatch({ type: FETCH_INIT, payload: {} }); @@ -121,7 +119,6 @@ export const useCaseApi = (): [CasesState, Dispatch>] dispatch({ type: FETCH_SUCCESS, payload: await result.json() }); } } catch (error) { - console.log('ERRROR', error); if (!didCancel) { dispatch({ type: FETCH_FAILURE, payload: {} }); } From d6160de9deb5ecc2871ae0632027d61d26a0b275 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 22 Jan 2020 10:05:29 -0700 Subject: [PATCH 13/67] server down so removing server changes =P --- .../page/case/case_view/index_hook.tsx | 97 +++++++++++++++ .../page/case/cases_table/table_hook.tsx | 6 +- .../siem/public/hooks/case/use_get_case.tsx | 111 ++++++++++++++++++ .../{use_case_api.tsx => use_get_cases.tsx} | 4 +- .../siem/public/pages/case/case_details.tsx | 2 +- 5 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx create mode 100644 x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx rename x-pack/legacy/plugins/siem/public/hooks/case/{use_case_api.tsx => use_get_cases.tsx} (95%) diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx new file mode 100644 index 00000000000000..2ece312c4641ba --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx @@ -0,0 +1,97 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { HeaderPage } from '../../../header_page'; +import * as i18n from '../../../../pages/case/translations'; +import { getCaseUrl } from '../../../link_to'; +import { useGetCase } from '../../../../hooks/case/use_get_case'; + +interface Props { + caseId: string; +} + +const getDictionary = ( + title: React.ReactNode, + definition: string | number | JSX.Element | null, + key: number +) => { + return definition ? ( + +
{title}
+
{definition}
+
+ ) : null; +}; +export const CaseView = React.memo(({ caseId }: Props) => { + const [{ data, isLoading, isError }] = useGetCase(caseId); + if (isError) { + return null; + } + const caseDetailsDefinitions = [ + { + title: , + definition: data.attributes.description, + }, + { + title: , + definition: data.attributes.case_type, + }, + { + title: , + definition: data.attributes.state, + }, + { + title: , + definition: data.updated_at, + }, + { + title: , + definition: data.attributes.created_at, + }, + { + title: , + definition: data.attributes.created_by.username, + }, + { + title: , + definition: + data.attributes.tags.length > 0 ? ( +
    + {data.attributes.tags.map((tag, key) => ( +
  • {tag}
  • + ))} +
+ ) : null, + }, + ]; + return isLoading ? ( + + + + + + ) : ( + + + {i18n.BACK_LABEL} + + + +
+ {caseDetailsDefinitions.map((dictionaryItem, key) => + getDictionary(dictionaryItem.title, dictionaryItem.definition, key) + )} +
+
+
+ ); +}); + +CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx index 1b02696b1dfefb..f560491143dedc 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -15,7 +15,7 @@ import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table' import { getCasesColumns } from './columns'; import { Direction, SortFieldCase } from '../../../../graphql/types'; import { caseActions, caseModel } from '../../../../store/case'; -import { useCaseApi } from '../../../../hooks/case/use_case_api'; +import { useGetCases } from '../../../../hooks/case/use_get_cases'; interface CasesTableProps { id: string; @@ -49,7 +49,7 @@ export const CasesPaginatedTableComponent = React.memo( table: { page, perPage, sortOrder, sortField }, }, doFetch, - ] = useCaseApi(); + ] = useGetCases(); const updateActivePage = (newPage: number) => doFetch({ diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx new file mode 100644 index 00000000000000..6495690be4a6f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx @@ -0,0 +1,111 @@ +/* + * 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 { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; + +import chrome from 'ui/chrome'; +import { CaseSavedObject } from '../../components/page/case/types'; +import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; + +interface CaseState { + data: CaseSavedObject; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: CaseSavedObject; +} + +const dataFetchReducer = (state: CaseState, action: Action): CaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + const getSavedObject = (a: Action['payload']) => a as CaseSavedObject; + return { + ...state, + isLoading: false, + isError: false, + data: getSavedObject(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: CaseSavedObject = { + attributes: { + case_type: '', + created_at: 0, + created_by: { + username: '', + }, + description: '', + state: '', + tags: [], + title: '', + }, + id: '', + type: '', + updated_at: '', + version: '', +}; +export const useGetCase = (initialCaseId: string): [CaseState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [caseId, setCaseId] = useState(initialCaseId); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const result = await fetch( + `${chrome.getBasePath()}/api/cases/${caseId}?includeComments=false`, + { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + }, + } + ); + if (!didCancel) { + const resultJson = await result.json() + console.log('resultJson', resultJson); + if (resultJson.statusCode === 404) { + return dispatch({ type: FETCH_FAILURE }); + } + dispatch({ type: FETCH_SUCCESS, payload: resultJson }); + } + } catch (error) { + if (!didCancel) { + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + setCaseId(initialCaseId); + }; + }, [caseId]); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx rename to x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx index 6b69c67cf48259..667ac8bcce0442 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/use_case_api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx @@ -49,7 +49,7 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { isError: false, }; case FETCH_SUCCESS: - const getSavedObject = a => a as CasesSavedObjects; + const getSavedObject = (a: Action['payload']) => a as CasesSavedObjects; return { ...state, isLoading: false, @@ -80,7 +80,7 @@ const initialData: CasesSavedObjects = { total: 0, saved_objects: [], }; -export const useCaseApi = (): [CasesState, Dispatch>] => { +export const useGetCases = (): [CasesState, Dispatch>] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index 1123cc7f8860fb..f52643678ce9b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../lib/kibana'; import { EmptyPage } from '../../components/empty_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CaseView } from '../../components/page/case/case_view'; +import { CaseView } from '../../components/page/case/case_view/index_hook'; import * as i18n from './translations'; import { SpyRoute } from '../../utils/route/spy_routes'; From 18e621eb0e66d9d0d1e3c4e3e6d3a06a55f6639d Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 22 Jan 2020 10:53:33 -0700 Subject: [PATCH 14/67] remove case graphql --- .../components/page/case/case_view/index.tsx | 150 +++--- .../page/case/case_view/index_hook.tsx | 97 ---- .../page/case/cases_table/columns.tsx | 2 +- .../page/case/cases_table/index.tsx | 4 +- .../page/case/cases_table/table.tsx | 201 +++----- .../page/case/cases_table/table_hook.tsx | 123 ----- .../case/get_case/index.gql_query.ts | 30 -- .../public/containers/case/get_case/index.tsx | 73 --- .../case/get_cases/index.gql_query.ts | 35 -- .../containers/case/get_cases/index.tsx | 112 ---- .../siem/public/graphql/introspection.json | 487 +----------------- .../plugins/siem/public/graphql/types.ts | 204 +------- .../{components/page => hooks}/case/types.ts | 19 +- .../siem/public/hooks/case/use_get_case.tsx | 9 +- .../siem/public/hooks/case/use_get_cases.tsx | 17 +- .../plugins/siem/public/mock/global_state.ts | 15 - .../siem/public/pages/case/case_details.tsx | 2 +- .../plugins/siem/public/store/case/actions.ts | 15 - .../plugins/siem/public/store/case/index.ts | 12 - .../plugins/siem/public/store/case/model.ts | 37 -- .../plugins/siem/public/store/case/reducer.ts | 44 -- .../siem/public/store/case/selectors.ts | 12 - .../legacy/plugins/siem/public/store/model.ts | 1 - .../plugins/siem/public/store/reducer.ts | 4 - .../plugins/siem/server/graphql/case/index.ts | 8 - .../siem/server/graphql/case/resolvers.ts | 50 -- .../siem/server/graphql/case/schema.gql.ts | 70 --- .../plugins/siem/server/graphql/index.ts | 2 - .../plugins/siem/server/graphql/types.ts | 273 +--------- .../legacy/plugins/siem/server/init_server.ts | 2 - .../siem/server/lib/case/saved_object.ts | 125 ----- .../plugins/siem/server/lib/compose/kibana.ts | 3 - .../legacy/plugins/siem/server/lib/types.ts | 2 - 33 files changed, 232 insertions(+), 2008 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/containers/case/get_case/index.gql_query.ts delete mode 100644 x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts delete mode 100644 x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx rename x-pack/legacy/plugins/siem/public/{components/page => hooks}/case/types.ts (81%) delete mode 100644 x-pack/legacy/plugins/siem/public/store/case/actions.ts delete mode 100644 x-pack/legacy/plugins/siem/public/store/case/index.ts delete mode 100644 x-pack/legacy/plugins/siem/public/store/case/model.ts delete mode 100644 x-pack/legacy/plugins/siem/public/store/case/reducer.ts delete mode 100644 x-pack/legacy/plugins/siem/public/store/case/selectors.ts delete mode 100644 x-pack/legacy/plugins/siem/server/graphql/case/index.ts delete mode 100644 x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts delete mode 100644 x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx index 2d5816075929db..2ece312c4641ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx @@ -4,84 +4,94 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CaseQuery } from '../../../../containers/case/get_case'; import { HeaderPage } from '../../../header_page'; import * as i18n from '../../../../pages/case/translations'; import { getCaseUrl } from '../../../link_to'; +import { useGetCase } from '../../../../hooks/case/use_get_case'; interface Props { caseId: string; } -export const CaseView = React.memo(({ caseId }: Props) => ( - - - {children => ( - <> - - {i18n.BACK_LABEL} - - - -
-
- -
-
{children.case.attributes.description}
-
- -
-
{children.case.attributes.case_type}
-
- -
-
{children.case.attributes.state}
-
- -
-
{children.case.updated_at}
-
- -
-
{children.case.attributes.created_at}
-
- -
-
{children.case.attributes.created_by.username}
-
- -
-
-
    - {children.case.attributes.tags.map((tag, key) => ( -
  • {tag}
  • - ))} -
-
-
-
- - )} -
-
-)); + +const getDictionary = ( + title: React.ReactNode, + definition: string | number | JSX.Element | null, + key: number +) => { + return definition ? ( + +
{title}
+
{definition}
+
+ ) : null; +}; +export const CaseView = React.memo(({ caseId }: Props) => { + const [{ data, isLoading, isError }] = useGetCase(caseId); + if (isError) { + return null; + } + const caseDetailsDefinitions = [ + { + title: , + definition: data.attributes.description, + }, + { + title: , + definition: data.attributes.case_type, + }, + { + title: , + definition: data.attributes.state, + }, + { + title: , + definition: data.updated_at, + }, + { + title: , + definition: data.attributes.created_at, + }, + { + title: , + definition: data.attributes.created_by.username, + }, + { + title: , + definition: + data.attributes.tags.length > 0 ? ( +
    + {data.attributes.tags.map((tag, key) => ( +
  • {tag}
  • + ))} +
+ ) : null, + }, + ]; + return isLoading ? ( + + + + + + ) : ( + + + {i18n.BACK_LABEL} + + + +
+ {caseDetailsDefinitions.map((dictionaryItem, key) => + getDictionary(dictionaryItem.title, dictionaryItem.definition, key) + )} +
+
+
+ ); +}); CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx deleted file mode 100644 index 2ece312c4641ba..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index_hook.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { HeaderPage } from '../../../header_page'; -import * as i18n from '../../../../pages/case/translations'; -import { getCaseUrl } from '../../../link_to'; -import { useGetCase } from '../../../../hooks/case/use_get_case'; - -interface Props { - caseId: string; -} - -const getDictionary = ( - title: React.ReactNode, - definition: string | number | JSX.Element | null, - key: number -) => { - return definition ? ( - -
{title}
-
{definition}
-
- ) : null; -}; -export const CaseView = React.memo(({ caseId }: Props) => { - const [{ data, isLoading, isError }] = useGetCase(caseId); - if (isError) { - return null; - } - const caseDetailsDefinitions = [ - { - title: , - definition: data.attributes.description, - }, - { - title: , - definition: data.attributes.case_type, - }, - { - title: , - definition: data.attributes.state, - }, - { - title: , - definition: data.updated_at, - }, - { - title: , - definition: data.attributes.created_at, - }, - { - title: , - definition: data.attributes.created_by.username, - }, - { - title: , - definition: - data.attributes.tags.length > 0 ? ( -
    - {data.attributes.tags.map((tag, key) => ( -
  • {tag}
  • - ))} -
- ) : null, - }, - ]; - return isLoading ? ( - - - - - - ) : ( - - - {i18n.BACK_LABEL} - - - -
- {caseDetailsDefinitions.map((dictionaryItem, key) => - getDictionary(dictionaryItem.title, dictionaryItem.definition, key) - )} -
-
-
- ); -}); - -CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx index 13ab08b8bf1002..3a89d12441ab78 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { getEmptyTagValue } from '../../../empty_value'; import { Columns } from '../../../paginated_table'; -import { CaseSavedObject } from '../../../../graphql/types'; +import { CaseSavedObject } from '../../../../hooks/case/types'; import { FormattedRelativePreferenceDate } from '../../../formatted_date'; import { CaseDetailsLink } from '../../../links'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx index 1facfc1c4541c6..a11be19ded03b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; -import { CasesPaginatedTable } from './table_hook'; +import { CasesPaginatedTable } from './table'; export const CasesTable = React.memo(() => { return ( - + ); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx index ed9326077a92d9..c38124baabf275 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx @@ -4,129 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ActionCreator } from 'typescript-fsa'; -import { Criteria, PaginatedTable } from '../../../paginated_table'; +import * as i18n from './translations'; +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; import { getCasesColumns } from './columns'; -import { CasesSavedObjects, Direction, SortCase, SortFieldCase } from '../../../../graphql/types'; -import { caseActions, caseSelectors, caseModel } from '../../../../store/case'; -import { State } from '../../../../store'; +import { Direction, SortFieldCase } from '../../../../hooks/case/types'; +import { useGetCases } from '../../../../hooks/case/use_get_cases'; -interface CasesTableProps { - id: string; - cases: CasesSavedObjects; - loading: boolean; -} +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; +export const CasesPaginatedTable = React.memo(() => { + const [ + { + data, + isLoading, + isError, + table: { page, perPage, sortOrder, sortField }, + }, + doFetch, + ] = useGetCases(); -interface CasesTableReduxProps { - activePage: number; - limit: number; - sort: SortCase; -} + const updateActivePage = (newPage: number) => + doFetch({ + page: newPage + 1, + }); -interface CasesTableDispatchProps { - updateCaseTable: ActionCreator<{ - tableType: caseModel.CaseTableType; - updates: caseModel.TableUpdates; - }>; -} + const updateLimitPagination = (newLimit: number) => + doFetch({ + page: 1, // reset to first page + perPage: newLimit, + }); -type CasesPaginatedTableProps = CasesTableReduxProps & CasesTableProps & CasesTableDispatchProps; + const onChange = (criteria: Criteria) => { + if (criteria.sort != null && criteria.sort.direction !== sortOrder) { + let newSort; + switch (criteria.sort.field) { + case 'attributes.state': + newSort = SortFieldCase.state; + break; + case 'attributes.created_at': + newSort = SortFieldCase.createdAt; + break; + case 'updated_at': + newSort = SortFieldCase.updatedAt; + break; + default: + newSort = SortFieldCase.createdAt; + } + doFetch({ + sortField: newSort, + sortOrder: criteria.sort.direction as Direction, + }); + } + }; -export const CasesPaginatedTableComponent = React.memo( - ({ id, cases, loading, sort, activePage, limit, updateCaseTable }: CasesPaginatedTableProps) => { - const tableType = caseModel.CaseTableType.cases; - const updateActivePage = useCallback( - newPage => - updateCaseTable({ - tableType, - updates: { activePage: newPage }, - }), - [updateCaseTable, tableType] - ); + const sorting = { field: `attributes.${sortField}`, direction: sortOrder }; + return isError ? null : ( + + } + headerUnit={i18n.UNIT(data.total)} + hideInspect={true} + id={'getCasesTable'} + itemsPerRow={rowItems} + limit={perPage} + limitResetsActivePage={false} + loading={isLoading} + loadPage={newPage => newPage} + onChange={onChange} + pageOfItems={data.saved_objects} + showMorePagesIndicator={false} + sorting={sorting} + totalCount={data.total} + updateActivePage={updateActivePage} + updateLimitPagination={updateLimitPagination} + /> + ); +}); - const updateLimitPagination = useCallback( - newLimit => - updateCaseTable({ - tableType, - updates: { limit: newLimit }, - }), - [updateCaseTable, tableType] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - console.log('Critera sort', criteria.sort); - console.log('sortsortsort', sort); - if (criteria.sort != null && criteria.sort.direction !== sort.direction) { - let newSort; - switch (criteria.sort.field) { - case 'attributes.state': - newSort = SortFieldCase.state; - break; - case 'attributes.created_at': - newSort = SortFieldCase.created_at; - break; - case 'updated_at': - newSort = SortFieldCase.updated_at; - break; - default: - newSort = SortFieldCase.created_at; - } - console.log('newSort', newSort) - updateCaseTable({ - tableType, - updates: { - sort: { - field: newSort, - direction: criteria.sort.direction as Direction, - }, - }, - }); - } - }, - [tableType, sort.direction, updateCaseTable] - ); - - const sorting = { field: `attributes.${sort.field}`, direction: sort.direction }; - - return ( - - } - headerUnit={'cases'} - hideInspect={true} - id={id} - limit={limit} - loading={loading} - loadPage={page => updateActivePage(page)} - onChange={onChange} - pageOfItems={cases.saved_objects} - showMorePagesIndicator={false} - sorting={sorting} - totalCount={cases.total} - updateActivePage={updateActivePage} - updateLimitPagination={updateLimitPagination} - /> - ); - } -); - -CasesPaginatedTableComponent.displayName = 'CasesPaginatedTableComponent'; -const makeMapStateToProps = () => { - const getCasesSelector = caseSelectors.casesSelector(); - return (state: State) => getCasesSelector(state); -}; -export const CasesPaginatedTable = compose>( - connect(makeMapStateToProps, { - updateCaseTable: caseActions.updateCaseTable, - }) -)(CasesPaginatedTableComponent); +CasesPaginatedTable.displayName = 'CasesPaginatedTable'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx deleted file mode 100644 index f560491143dedc..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table_hook.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ActionCreator } from 'typescript-fsa'; -import * as i18n from './translations'; -import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; - -import { getCasesColumns } from './columns'; -import { Direction, SortFieldCase } from '../../../../graphql/types'; -import { caseActions, caseModel } from '../../../../store/case'; -import { useGetCases } from '../../../../hooks/case/use_get_cases'; - -interface CasesTableProps { - id: string; -} - -interface CasesTableDispatchProps { - updateCaseTable: ActionCreator<{ - tableType: caseModel.CaseTableType; - updates: caseModel.TableUpdates; - }>; -} - -type CasesPaginatedTableProps = CasesTableProps & CasesTableDispatchProps; -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; -export const CasesPaginatedTableComponent = React.memo( - ({ id, updateCaseTable }: CasesPaginatedTableProps) => { - const [ - { - data, - isLoading, - isError, - table: { page, perPage, sortOrder, sortField }, - }, - doFetch, - ] = useGetCases(); - - const updateActivePage = (newPage: number) => - doFetch({ - page: newPage + 1, - }); - - const updateLimitPagination = (newLimit: number) => - doFetch({ - page: 1, // reset to first page - perPage: newLimit, - }); - - const onChange = (criteria: Criteria) => { - if (criteria.sort != null && criteria.sort.direction !== sortOrder) { - let newSort; - switch (criteria.sort.field) { - case 'attributes.state': - newSort = SortFieldCase.state; - break; - case 'attributes.created_at': - newSort = SortFieldCase.created_at; - break; - case 'updated_at': - newSort = SortFieldCase.updated_at; - break; - default: - newSort = SortFieldCase.created_at; - } - doFetch({ - sortField: newSort, - sortOrder: criteria.sort.direction as Direction, - }); - } - }; - - const sorting = { field: `attributes.${sortField}`, direction: sortOrder }; - return isError ? null : ( - - } - headerUnit={i18n.UNIT(data.total)} - hideInspect={true} - id={id} - itemsPerRow={rowItems} - limit={perPage} - limitResetsActivePage={false} - loading={isLoading} - loadPage={newPage => newPage} - onChange={onChange} - pageOfItems={data.saved_objects} - showMorePagesIndicator={false} - sorting={sorting} - totalCount={data.total} - updateActivePage={updateActivePage} - updateLimitPagination={updateLimitPagination} - /> - ); - } -); - -CasesPaginatedTableComponent.displayName = 'CasesPaginatedTableComponent'; - -export const CasesPaginatedTable = compose>( - connect(null, { - updateCaseTable: caseActions.updateCaseTable, - }) -)(CasesPaginatedTableComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.gql_query.ts deleted file mode 100644 index aa90ff6a666daa..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.gql_query.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const caseQuery = gql` - query GetCaseQuery($caseId: ID!) { - getCase(caseId: $caseId) { - id - type - updated_at - version - attributes { - case_type - created_at - created_by { - username - full_name - } - description - state - tags - title - } - } - } -`; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx deleted file mode 100644 index 33f7bc6842f4f1..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_case/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { GetCaseQuery, CaseSavedObject } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; -import { getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { caseQuery } from './index.gql_query'; - -const ID = 'caseQuery'; - -export interface CaseArgs { - id: string; - case: CaseSavedObject; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface CaseProps extends QueryTemplateProps { - children: (args: CaseArgs) => React.ReactNode; - caseId: string; -} - -const CaseComponentQuery = React.memo(({ children, skip, caseId }) => ( - - query={caseQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - caseId, - }} - > - {({ data, loading, refetch }) => { - const init: CaseSavedObject = { - id: caseId, - type: '', - updated_at: '', - version: '', - attributes: { - case_type: '', - created_at: 1234235345, - created_by: { - username: '', - full_name: null, - }, - description: '', - state: 'open', - tags: [], - title: '', - }, - }; - const caseData: CaseSavedObject = getOr(init, 'getCase', data); - return children({ - id: ID, - case: caseData, - loading, - refetch, - }); - }} - -)); - -CaseComponentQuery.displayName = 'CaseComponentQuery'; - -export const CaseQuery = CaseComponentQuery; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts deleted file mode 100644 index aead147b5c87b8..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.gql_query.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const casesQuery = gql` - query GetCasesQuery($pageInfo: PageInfoCase!, $search: String, $sort: SortCase) { - getCases(pageInfo: $pageInfo, search: $search, sort: $sort) { - page - per_page - total - saved_objects { - id - type - updated_at - version - attributes { - case_type - created_at - created_by { - username - full_name - } - description - state - tags - title - } - } - } - } -`; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx b/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx deleted file mode 100644 index f4e0178151487e..00000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/get_cases/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { GetCasesQuery, CasesSavedObjects, SortCase } from '../../../graphql/types'; -import { inputsModel, State } from '../../../store'; -import { getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { casesQuery } from './index.gql_query'; -import { caseSelectors } from '../../../store/case'; -import { QueryTemplatePaginated } from '../../query_template_paginated'; - -const ID = 'casesQuery'; - -export interface CasesArgs { - id: string; - cases: CasesSavedObjects; - loading: boolean; - loadPage: (newActivePage: number) => void; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: CasesArgs) => React.ReactNode; - search?: string; -} - -export interface CasesQueryReduxProps { - activePage: number; - limit: number; - sort: SortCase; -} - -type CasesComponentQueryProps = OwnProps & CasesQueryReduxProps; - -class CasesComponentQuery extends QueryTemplatePaginated< - CasesComponentQueryProps, - GetCasesQuery.Query, - GetCasesQuery.Variables -> { - public render() { - const { activePage, children, limit, search, skip, sort } = this.props; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={casesQuery} - skip={skip} - variables={{ - pageInfo: { - pageIndex: activePage, - pageSize: limit, - }, - search, - sort, - }} - > - {({ data, loading, refetch }) => { - const init: CasesSavedObjects = { - page: 0, - per_page: 0, - total: 0, - saved_objects: [ - { - id: '000', - type: '', - updated_at: '', - version: '', - attributes: { - case_type: '', - created_at: 1234235345, - created_by: { - username: '', - full_name: null, - }, - description: '', - state: 'open', - tags: [], - title: '', - }, - }, - ], - }; - const caseData: CasesSavedObjects = getOr(init, 'getCases', data); - return children({ - id: ID, - cases: caseData, - loading, - loadPage: this.wrappedLoadMore, - refetch, - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getCasesSelector = caseSelectors.casesSelector(); - return (state: State) => getCasesSelector(state); -}; -export const CasesQuery = compose>(connect(makeMapStateToProps))( - CasesComponentQuery -); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index a06fa3d08aefda..7b9842fa2c2bce 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -9,60 +9,6 @@ "name": "Query", "description": "", "fields": [ - { - "name": "getCase", - "description": "", - "args": [ - { - "name": "caseId", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CaseSavedObject", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getCases", - "description": "", - "args": [ - { - "name": "pageInfo", - "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "PageInfoCase", "ofType": null }, - "defaultValue": null - }, - { - "name": "search", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "sort", - "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "SortCase", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CasesSavedObjects", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "getNote", "description": "", @@ -331,82 +277,35 @@ }, { "kind": "OBJECT", - "name": "CaseSavedObject", + "name": "NoteResult", "description": "", "fields": [ { - "name": "attributes", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CaseResult", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", + "name": "eventId", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "type", + "name": "note", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "updated_at", + "name": "timelineId", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "version", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CaseResult", - "description": "", - "fields": [ - { - "name": "case_type", + "name": "noteId", "description": "", "args": [], "type": { @@ -418,78 +317,50 @@ "deprecationReason": null }, { - "name": "created_at", + "name": "created", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "created_by", + "name": "createdBy", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "ElasticUser", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "description", + "name": "timelineVersion", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "state", + "name": "updated", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "tags", + "name": "updatedBy", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "title", + "name": "version", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -519,40 +390,9 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "ElasticUser", - "description": "", - "fields": [ - { - "name": "username", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "full_name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "INPUT_OBJECT", - "name": "PageInfoCase", + "name": "PageInfoNote", "description": "", "fields": null, "inputFields": [ @@ -583,22 +423,22 @@ }, { "kind": "INPUT_OBJECT", - "name": "SortCase", + "name": "SortNote", "description": "", "fields": null, "inputFields": [ { - "name": "field", + "name": "sortField", "description": "", "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "ENUM", "name": "SortFieldCase", "ofType": null } + "ofType": { "kind": "ENUM", "name": "SortFieldNote", "ofType": null } }, "defaultValue": null }, { - "name": "direction", + "name": "sortOrder", "description": "", "type": { "kind": "NON_NULL", @@ -614,25 +454,19 @@ }, { "kind": "ENUM", - "name": "SortFieldCase", + "name": "SortFieldNote", "description": "", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { - "name": "created_at", + "name": "updatedBy", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "state", "description": "", "isDeprecated": false, "deprecationReason": null }, - { - "name": "updated_at", - "description": "", - "isDeprecated": false, - "deprecationReason": null - } + { "name": "updated", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null }, @@ -649,244 +483,6 @@ ], "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "CasesSavedObjects", - "description": "", - "fields": [ - { - "name": "saved_objects", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CaseSavedObject", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "page", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "per_page", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "total", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "NoteResult", - "description": "", - "fields": [ - { - "name": "eventId", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "note", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timelineId", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "noteId", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "created", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdBy", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timelineVersion", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updated", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedBy", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "version", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "PageInfoNote", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "pageIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "pageSize", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SortNote", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "sortField", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "SortFieldNote", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "sortOrder", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "SortFieldNote", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "updatedBy", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "updated", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, { "kind": "OBJECT", "name": "ResponseNotes", @@ -11123,33 +10719,6 @@ "name": "Mutation", "description": "", "fields": [ - { - "name": "deleteCase", - "description": "", - "args": [ - { - "name": "id", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "persistNote", "description": "Persists a note", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index dcc9180af0f693..b13e295a8e1683 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -8,18 +8,6 @@ export type Maybe = T | null; -export interface PageInfoCase { - pageIndex: number; - - pageSize: number; -} - -export interface SortCase { - field: SortFieldCase; - - direction: Direction; -} - export interface PageInfoNote { pageIndex: number; @@ -279,10 +267,9 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldCase { - created_at = 'created_at', - state = 'state', - updated_at = 'updated_at', +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', } export enum Direction { @@ -290,11 +277,6 @@ export enum Direction { desc = 'desc', } -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', -} - export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -400,10 +382,6 @@ export type EsValue = any; // ==================================================== export interface Query { - getCase: CaseSavedObject; - - getCases: CasesSavedObjects; - getNote: NoteResult; getNotesByTimelineId: NoteResult[]; @@ -423,50 +401,6 @@ export interface Query { getAllTimeline: ResponseTimelines; } -export interface CaseSavedObject { - attributes: CaseResult; - - id: string; - - type: string; - - updated_at: string; - - version: string; -} - -export interface CaseResult { - case_type: string; - - created_at: number; - - created_by: ElasticUser; - - description: string; - - state: string; - - tags: (Maybe)[]; - - title: string; -} - -export interface ElasticUser { - username: string; - - full_name?: Maybe; -} - -export interface CasesSavedObjects { - saved_objects: (Maybe)[]; - - page: number; - - per_page: number; - - total: number; -} - export interface NoteResult { eventId?: Maybe; @@ -2166,7 +2100,6 @@ export interface ResponseTimelines { } export interface Mutation { - deleteCase?: Maybe; /** Persists a note */ persistNote: ResponseNote; @@ -2265,16 +2198,6 @@ export interface HostFields { // Arguments // ==================================================== -export interface GetCaseQueryArgs { - caseId: string; -} -export interface GetCasesQueryArgs { - pageInfo?: Maybe; - - search?: Maybe; - - sort?: Maybe; -} export interface GetNoteQueryArgs { id: string; } @@ -2591,9 +2514,6 @@ export interface IndicesExistSourceStatusArgs { export interface IndexFieldsSourceStatusArgs { defaultIndex: string[]; } -export interface DeleteCaseMutationArgs { - id: string[]; -} export interface PersistNoteMutationArgs { noteId?: Maybe; @@ -2779,124 +2699,6 @@ export namespace GetAuthenticationsQuery { }; } -export namespace GetCaseQuery { - export type Variables = { - caseId: string; - }; - - export type Query = { - __typename?: 'Query'; - - getCase: GetCase; - }; - - export type GetCase = { - __typename?: 'CaseSavedObject'; - - id: string; - - type: string; - - updated_at: string; - - version: string; - - attributes: Attributes; - }; - - export type Attributes = { - __typename?: 'CaseResult'; - - case_type: string; - - created_at: number; - - created_by: CreatedBy; - - description: string; - - state: string; - - tags: (Maybe)[]; - - title: string; - }; - - export type CreatedBy = { - __typename?: 'ElasticUser'; - - username: string; - - full_name: Maybe; - }; -} - -export namespace GetCasesQuery { - export type Variables = { - pageInfo: PageInfoCase; - search?: Maybe; - sort?: Maybe; - }; - - export type Query = { - __typename?: 'Query'; - - getCases: GetCases; - }; - - export type GetCases = { - __typename?: 'CasesSavedObjects'; - - page: number; - - per_page: number; - - total: number; - - saved_objects: (Maybe)[]; - }; - - export type SavedObjects = { - __typename?: 'CaseSavedObject'; - - id: string; - - type: string; - - updated_at: string; - - version: string; - - attributes: Attributes; - }; - - export type Attributes = { - __typename?: 'CaseResult'; - - case_type: string; - - created_at: number; - - created_by: CreatedBy; - - description: string; - - state: string; - - tags: (Maybe)[]; - - title: string; - }; - - export type CreatedBy = { - __typename?: 'ElasticUser'; - - username: string; - - full_name: Maybe; - }; -} - export namespace GetLastEventTimeQuery { export type Variables = { sourceId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/types.ts b/x-pack/legacy/plugins/siem/public/hooks/case/types.ts similarity index 81% rename from x-pack/legacy/plugins/siem/public/components/page/case/types.ts rename to x-pack/legacy/plugins/siem/public/hooks/case/types.ts index 1d0c38c5c29b3f..713d0bd8d24157 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/case/types.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticUser, Maybe } from '../../../graphql/types'; import { SavedObjectsBaseOptions } from 'kibana/server'; - +import { Direction, Maybe } from '../../graphql/types'; +export { Direction }; export interface CasesSavedObjects { saved_objects: Array>; @@ -43,7 +43,15 @@ export interface CaseResult { title: string; } - +export interface SortCase { + field: SortFieldCase; + direction: Direction; +} +export enum SortFieldCase { + createdAt = 'created_at', + state = 'state', + updatedAt = 'updated_at', +} export interface CaseFindOptions extends SavedObjectsBaseOptions { page?: number; perPage?: number; @@ -63,3 +71,8 @@ export interface CaseFindOptions extends SavedObjectsBaseOptions { defaultSearchOperator?: 'AND' | 'OR'; filter?: string; } + +export interface ElasticUser { + username: string; + full_name?: Maybe; +} diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx index 6495690be4a6f9..fa9f14c9726c7c 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useEffect, useReducer, useState } from 'react'; import chrome from 'ui/chrome'; -import { CaseSavedObject } from '../../components/page/case/types'; +import { CaseSavedObject } from './types'; import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; interface CaseState { @@ -88,9 +88,8 @@ export const useGetCase = (initialCaseId: string): [CaseState] => { } ); if (!didCancel) { - const resultJson = await result.json() - console.log('resultJson', resultJson); - if (resultJson.statusCode === 404) { + const resultJson = await result.json(); + if (resultJson.statusCode >= 400) { return dispatch({ type: FETCH_FAILURE }); } dispatch({ type: FETCH_SUCCESS, payload: resultJson }); diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx index 667ac8bcce0442..2645cd5cd3ad8b 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx @@ -7,10 +7,9 @@ import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; import chrome from 'ui/chrome'; -import { CasesSavedObjects } from '../../components/page/case/types'; import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS, UPDATE_TABLE } from './constants'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; -import { Direction, SortFieldCase } from '../../graphql/types'; +import { CasesSavedObjects, Direction, SortFieldCase } from './types'; interface TableArgs { page: number; @@ -37,7 +36,7 @@ interface PayloadObj { } interface Action { type: string; - payload: CasesSavedObjects | QueryArgs | PayloadObj; + payload?: CasesSavedObjects | QueryArgs | PayloadObj; } const dataFetchReducer = (state: CasesState, action: Action): CasesState => { @@ -88,7 +87,7 @@ export const useGetCases = (): [CasesState, Dispatch>] table: { page: DEFAULT_TABLE_ACTIVE_PAGE + 1, perPage: DEFAULT_TABLE_LIMIT, - sortField: SortFieldCase.created_at, + sortField: SortFieldCase.createdAt, sortOrder: Direction.desc, }, }); @@ -101,7 +100,7 @@ export const useGetCases = (): [CasesState, Dispatch>] useEffect(() => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT, payload: {} }); + dispatch({ type: FETCH_INIT }); try { const queryParams = Object.entries(state.table).reduce((acc, [key, value]) => { return `${acc}${key}=${value}&`; @@ -116,11 +115,15 @@ export const useGetCases = (): [CasesState, Dispatch>] }, }); if (!didCancel) { - dispatch({ type: FETCH_SUCCESS, payload: await result.json() }); + const resultJson = await result.json(); + if (resultJson.statusCode >= 400) { + return dispatch({ type: FETCH_FAILURE }); + } + dispatch({ type: FETCH_SUCCESS, payload: resultJson }); } } catch (error) { if (!didCancel) { - dispatch({ type: FETCH_FAILURE, payload: {} }); + dispatch({ type: FETCH_FAILURE }); } } }; diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index 7450b82f99d759..31e203d0803222 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -11,7 +11,6 @@ import { HostsFields, NetworkDnsFields, NetworkTopTablesFields, - SortFieldCase, TlsFields, UsersFields, } from '../graphql/types'; @@ -33,20 +32,6 @@ export const mockGlobalState: State = { { id: 'error-id-2', title: 'title-2', message: ['error-message-2'] }, ], }, - case: { - page: { - queries: { - cases: { - activePage: 0, - limit: 10, - sort: { - direction: Direction.desc, - field: SortFieldCase.created_at, - }, - }, - }, - }, - }, hosts: { page: { queries: { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index f52643678ce9b6..1123cc7f8860fb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../lib/kibana'; import { EmptyPage } from '../../components/empty_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CaseView } from '../../components/page/case/case_view/index_hook'; +import { CaseView } from '../../components/page/case/case_view'; import * as i18n from './translations'; import { SpyRoute } from '../../utils/route/spy_routes'; diff --git a/x-pack/legacy/plugins/siem/public/store/case/actions.ts b/x-pack/legacy/plugins/siem/public/store/case/actions.ts deleted file mode 100644 index 7a013847299728..00000000000000 --- a/x-pack/legacy/plugins/siem/public/store/case/actions.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import actionCreatorFactory from 'typescript-fsa'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/case'); -import { caseModel } from './index'; - -export const updateCaseTable = actionCreator<{ - tableType: caseModel.CaseTableType; - updates: caseModel.TableUpdates; -}>('UPDATE_CASE_TABLE'); diff --git a/x-pack/legacy/plugins/siem/public/store/case/index.ts b/x-pack/legacy/plugins/siem/public/store/case/index.ts deleted file mode 100644 index a92c163d978e1a..00000000000000 --- a/x-pack/legacy/plugins/siem/public/store/case/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as caseActions from './actions'; -import * as caseModel from './model'; -import * as caseSelectors from './selectors'; - -export { caseActions, caseModel, caseSelectors }; -export * from './reducer'; diff --git a/x-pack/legacy/plugins/siem/public/store/case/model.ts b/x-pack/legacy/plugins/siem/public/store/case/model.ts deleted file mode 100644 index a21832cc27610a..00000000000000 --- a/x-pack/legacy/plugins/siem/public/store/case/model.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SortCase } from '../../graphql/types'; - -export enum CaseTableType { - cases = 'cases', -} -export interface TableUpdates { - activePage?: number; - limit?: number; - sort?: SortCase; -} - -export interface BasicQueryPaginated { - activePage: number; - limit: number; -} - -export interface CasesQuery extends BasicQueryPaginated { - sort: SortCase; -} - -export interface CaseQueries { - [CaseTableType.cases]: CasesQuery; -} - -export interface CasePageModel { - queries: CaseQueries; -} - -export interface CaseModel { - page: CasePageModel; -} diff --git a/x-pack/legacy/plugins/siem/public/store/case/reducer.ts b/x-pack/legacy/plugins/siem/public/store/case/reducer.ts deleted file mode 100644 index afb2b41db4a9be..00000000000000 --- a/x-pack/legacy/plugins/siem/public/store/case/reducer.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { CaseModel, CaseTableType } from './model'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../constants'; -import { Direction, SortFieldCase } from '../../graphql/types'; -import { updateCaseTable } from './actions'; - -export type CaseState = CaseModel; - -export const initialCaseState: CaseState = { - page: { - queries: { - [CaseTableType.cases]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: SortFieldCase.created_at, - direction: Direction.desc, - }, - }, - }, - }, -}; - -export const caseReducer = reducerWithInitialState(initialCaseState) - .case(updateCaseTable, (state, { tableType, updates }) => ({ - ...state, - page: { - ...state.page, - queries: { - ...state.page.queries, - [tableType]: { - ...state.page.queries[tableType], - ...updates, - }, - }, - }, - })) - .build(); diff --git a/x-pack/legacy/plugins/siem/public/store/case/selectors.ts b/x-pack/legacy/plugins/siem/public/store/case/selectors.ts deleted file mode 100644 index b22bb44905b936..00000000000000 --- a/x-pack/legacy/plugins/siem/public/store/case/selectors.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { createSelector } from 'reselect'; -import { State } from '../reducer'; -import { CasePageModel } from '../case/model'; - -const selectCasePage = (state: State): CasePageModel => state.case.page; -export const casesSelector = () => - createSelector(selectCasePage, casePage => casePage.queries.cases); diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts index 99ae3858d92ebc..9e9e663a59fe09 100644 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/model.ts @@ -5,7 +5,6 @@ */ export { appModel } from './app'; -export { caseModel } from './case'; export { dragAndDropModel } from './drag_and_drop'; export { hostsModel } from './hosts'; export { inputsModel } from './inputs'; diff --git a/x-pack/legacy/plugins/siem/public/store/reducer.ts b/x-pack/legacy/plugins/siem/public/store/reducer.ts index 035fd5c09467c1..3c93dfa61b7683 100644 --- a/x-pack/legacy/plugins/siem/public/store/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/reducer.ts @@ -11,13 +11,11 @@ import { dragAndDropReducer, DragAndDropState, initialDragAndDropState } from '. import { hostsReducer, HostsState, initialHostsState } from './hosts'; import { initialInputsState, inputsReducer, InputsState } from './inputs'; import { initialNetworkState, networkReducer, NetworkState } from './network'; -import { initialCaseState, caseReducer, CaseState } from './case'; import { initialTimelineState, timelineReducer } from './timeline/reducer'; import { TimelineState } from './timeline/types'; export interface State { app: AppState; - case: CaseState; dragAndDrop: DragAndDropState; hosts: HostsState; inputs: InputsState; @@ -27,7 +25,6 @@ export interface State { export const initialState: State = { app: initialAppState, - case: initialCaseState, dragAndDrop: initialDragAndDropState, hosts: initialHostsState, inputs: initialInputsState, @@ -37,7 +34,6 @@ export const initialState: State = { export const reducer = combineReducers({ app: appReducer, - case: caseReducer, dragAndDrop: dragAndDropReducer, hosts: hostsReducer, inputs: inputsReducer, diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/index.ts b/x-pack/legacy/plugins/siem/server/graphql/case/index.ts deleted file mode 100644 index 77c2c5db7159f8..00000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/case/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createCaseResolvers } from './resolvers'; -export { caseSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts deleted file mode 100644 index 94ab2095131d3f..00000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/case/resolvers.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AppResolverWithFields, AppResolverOf } from '../../lib/framework'; -import { MutationResolvers, QueryResolvers } from '../types'; -import { Case } from '../../lib/case/saved_object'; - -export type QueryCaseResolver = AppResolverOf; - -export type QueryAllCaseResolver = AppResolverWithFields< - QueryResolvers.GetCasesResolver, - 'totalCount' | 'Case' ->; - -export type MutationDeleteCaseResolver = AppResolverOf; - -interface CaseResolversDeps { - case: Case; -} - -export const createCaseResolvers = ( - libs: CaseResolversDeps -): { - Query: { - getCase: QueryCaseResolver; - getCases: QueryAllCaseResolver; - }; - Mutation: { - deleteCase: MutationDeleteCaseResolver; - }; -} => ({ - Query: { - async getCase(root, args, { req }) { - return libs.case.getCase(req, args.caseId); - }, - async getCases(root, args, { req }) { - return libs.case.getCases(req, args.pageInfo || null, args.search || null, args.sort || null); - }, - }, - Mutation: { - async deleteCase(root, args, { req }) { - await libs.case.deleteCase(req, args.id); - - return true; - }, - }, -}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts deleted file mode 100644 index e2a3b39b29d7f2..00000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/case/schema.gql.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const caseSchema = gql` - input PageInfoCase { - pageIndex: Float! - pageSize: Float! - } - - enum SortFieldCase { - created_at - state - updated_at - } - - input SortCase { - field: SortFieldCase! - direction: Direction! - } - ############### - #### QUERY #### - ############### - type ElasticUser { - username: String! - full_name: String - } - type CaseResult { - case_type: String! - created_at: Float! - created_by: ElasticUser! - description: String! - state: String! - tags: [String]! - title: String! - } - - type CaseSavedObject { - attributes: CaseResult! - id: String! - type: String! - updated_at: String! - version: String! - } - - type CasesSavedObjects { - saved_objects: [CaseSavedObject]! - page: Float! - per_page: Float! - total: Float! - } - - ######################### - #### Mutation/Query #### - ######################### - - extend type Query { - getCase(caseId: ID!): CaseSavedObject! - getCases(pageInfo: PageInfoCase, search: String, sort: SortCase): CasesSavedObjects! - } - - extend type Mutation { - deleteCase(id: [ID!]!): Boolean - } -`; - diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index ea27b77c705745..60853e2ce7bed4 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -9,7 +9,6 @@ import { sharedSchema } from '../../common/graphql/shared'; import { anomaliesSchema } from './anomalies'; import { authenticationsSchema } from './authentications'; -import { caseSchema } from './case'; import { ecsSchema } from './ecs'; import { eventsSchema } from './events'; import { hostsSchema } from './hosts'; @@ -36,7 +35,6 @@ export const schemas = [ alertsSchema, anomaliesSchema, authenticationsSchema, - caseSchema, ecsSchema, eventsSchema, dateSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index b181d259d2eee7..4a2119b6f76313 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -10,18 +10,6 @@ import { SiemContext } from '../lib/types'; export type Maybe = T | null; -export interface PageInfoCase { - pageIndex: number; - - pageSize: number; -} - -export interface SortCase { - field: SortFieldCase; - - direction: Direction; -} - export interface PageInfoNote { pageIndex: number; @@ -281,10 +269,9 @@ export interface FavoriteTimelineInput { favoriteDate?: Maybe; } -export enum SortFieldCase { - created_at = 'created_at', - state = 'state', - updated_at = 'updated_at', +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', } export enum Direction { @@ -292,11 +279,6 @@ export enum Direction { desc = 'desc', } -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', -} - export enum LastEventIndexKey { hostDetails = 'hostDetails', hosts = 'hosts', @@ -402,10 +384,6 @@ export type EsValue = any; // ==================================================== export interface Query { - getCase: CaseSavedObject; - - getCases: CasesSavedObjects; - getNote: NoteResult; getNotesByTimelineId: NoteResult[]; @@ -425,50 +403,6 @@ export interface Query { getAllTimeline: ResponseTimelines; } -export interface CaseSavedObject { - attributes: CaseResult; - - id: string; - - type: string; - - updated_at: string; - - version: string; -} - -export interface CaseResult { - case_type: string; - - created_at: number; - - created_by: ElasticUser; - - description: string; - - state: string; - - tags: (Maybe)[]; - - title: string; -} - -export interface ElasticUser { - username: string; - - full_name?: Maybe; -} - -export interface CasesSavedObjects { - saved_objects: (Maybe)[]; - - page: number; - - per_page: number; - - total: number; -} - export interface NoteResult { eventId?: Maybe; @@ -2168,7 +2102,6 @@ export interface ResponseTimelines { } export interface Mutation { - deleteCase?: Maybe; /** Persists a note */ persistNote: ResponseNote; @@ -2267,16 +2200,6 @@ export interface HostFields { // Arguments // ==================================================== -export interface GetCaseQueryArgs { - caseId: string; -} -export interface GetCasesQueryArgs { - pageInfo?: Maybe; - - search?: Maybe; - - sort?: Maybe; -} export interface GetNoteQueryArgs { id: string; } @@ -2593,9 +2516,6 @@ export interface IndicesExistSourceStatusArgs { export interface IndexFieldsSourceStatusArgs { defaultIndex: string[]; } -export interface DeleteCaseMutationArgs { - id: string[]; -} export interface PersistNoteMutationArgs { noteId?: Maybe; @@ -2684,10 +2604,6 @@ export type DirectiveResolverFn = ( export namespace QueryResolvers { export interface Resolvers { - getCase?: GetCaseResolver; - - getCases?: GetCasesResolver; - getNote?: GetNoteResolver; getNotesByTimelineId?: GetNotesByTimelineIdResolver; @@ -2711,29 +2627,6 @@ export namespace QueryResolvers { getAllTimeline?: GetAllTimelineResolver; } - export type GetCaseResolver = Resolver< - R, - Parent, - TContext, - GetCaseArgs - >; - export interface GetCaseArgs { - caseId: string; - } - - export type GetCasesResolver< - R = CasesSavedObjects, - Parent = {}, - TContext = SiemContext - > = Resolver; - export interface GetCasesArgs { - pageInfo?: Maybe; - - search?: Maybe; - - sort?: Maybe; - } - export type GetNoteResolver = Resolver< R, Parent, @@ -2825,152 +2718,6 @@ export namespace QueryResolvers { } } -export namespace CaseSavedObjectResolvers { - export interface Resolvers { - attributes?: AttributesResolver; - - id?: IdResolver; - - type?: TypeResolver; - - updated_at?: UpdatedAtResolver; - - version?: VersionResolver; - } - - export type AttributesResolver< - R = CaseResult, - Parent = CaseSavedObject, - TContext = SiemContext - > = Resolver; - export type IdResolver = Resolver< - R, - Parent, - TContext - >; - export type TypeResolver = Resolver< - R, - Parent, - TContext - >; - export type UpdatedAtResolver< - R = string, - Parent = CaseSavedObject, - TContext = SiemContext - > = Resolver; - export type VersionResolver< - R = string, - Parent = CaseSavedObject, - TContext = SiemContext - > = Resolver; -} - -export namespace CaseResultResolvers { - export interface Resolvers { - case_type?: CaseTypeResolver; - - created_at?: CreatedAtResolver; - - created_by?: CreatedByResolver; - - description?: DescriptionResolver; - - state?: StateResolver; - - tags?: TagsResolver<(Maybe)[], TypeParent, TContext>; - - title?: TitleResolver; - } - - export type CaseTypeResolver = Resolver< - R, - Parent, - TContext - >; - export type CreatedAtResolver = Resolver< - R, - Parent, - TContext - >; - export type CreatedByResolver< - R = ElasticUser, - Parent = CaseResult, - TContext = SiemContext - > = Resolver; - export type DescriptionResolver< - R = string, - Parent = CaseResult, - TContext = SiemContext - > = Resolver; - export type StateResolver = Resolver< - R, - Parent, - TContext - >; - export type TagsResolver< - R = (Maybe)[], - Parent = CaseResult, - TContext = SiemContext - > = Resolver; - export type TitleResolver = Resolver< - R, - Parent, - TContext - >; -} - -export namespace ElasticUserResolvers { - export interface Resolvers { - username?: UsernameResolver; - - full_name?: FullNameResolver, TypeParent, TContext>; - } - - export type UsernameResolver = Resolver< - R, - Parent, - TContext - >; - export type FullNameResolver< - R = Maybe, - Parent = ElasticUser, - TContext = SiemContext - > = Resolver; -} - -export namespace CasesSavedObjectsResolvers { - export interface Resolvers { - saved_objects?: SavedObjectsResolver<(Maybe)[], TypeParent, TContext>; - - page?: PageResolver; - - per_page?: PerPageResolver; - - total?: TotalResolver; - } - - export type SavedObjectsResolver< - R = (Maybe)[], - Parent = CasesSavedObjects, - TContext = SiemContext - > = Resolver; - export type PageResolver< - R = number, - Parent = CasesSavedObjects, - TContext = SiemContext - > = Resolver; - export type PerPageResolver< - R = number, - Parent = CasesSavedObjects, - TContext = SiemContext - > = Resolver; - export type TotalResolver< - R = number, - Parent = CasesSavedObjects, - TContext = SiemContext - > = Resolver; -} - export namespace NoteResultResolvers { export interface Resolvers { eventId?: EventIdResolver, TypeParent, TContext>; @@ -8999,7 +8746,6 @@ export namespace ResponseTimelinesResolvers { export namespace MutationResolvers { export interface Resolvers { - deleteCase?: DeleteCaseResolver, TypeParent, TContext>; /** Persists a note */ persistNote?: PersistNoteResolver; @@ -9032,15 +8778,6 @@ export namespace MutationResolvers { deleteTimeline?: DeleteTimelineResolver; } - export type DeleteCaseResolver< - R = Maybe, - Parent = {}, - TContext = SiemContext - > = Resolver; - export interface DeleteCaseArgs { - id: string[]; - } - export type PersistNoteResolver = Resolver< R, Parent, @@ -9441,10 +9178,6 @@ export interface EsValueScalarConfig extends GraphQLScalarTypeConfig = { Query?: QueryResolvers.Resolvers; - CaseSavedObject?: CaseSavedObjectResolvers.Resolvers; - CaseResult?: CaseResultResolvers.Resolvers; - ElasticUser?: ElasticUserResolvers.Resolvers; - CasesSavedObjects?: CasesSavedObjectsResolvers.Resolvers; NoteResult?: NoteResultResolvers.Resolvers; ResponseNotes?: ResponseNotesResolvers.Resolvers; PinnedEvent?: PinnedEventResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index 84ddf1d3695a69..1f4f1b176497fd 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -16,7 +16,6 @@ import { createKpiHostsResolvers } from './graphql/kpi_hosts'; import { createKpiNetworkResolvers } from './graphql/kpi_network'; import { createNetworkResolvers } from './graphql/network'; import { createNoteResolvers } from './graphql/note'; -import { createCaseResolvers } from './graphql/case'; import { createPinnedEventResolvers } from './graphql/pinned_event'; import { createOverviewResolvers } from './graphql/overview'; import { createScalarDateResolvers } from './graphql/scalar_date'; @@ -39,7 +38,6 @@ export const initServer = (libs: AppBackendLibs) => { createAlertsResolvers(libs) as IResolvers, createAnomaliesResolvers(libs) as IResolvers, createAuthenticationsResolvers(libs) as IResolvers, - createCaseResolvers(libs) as IResolvers, createEsValueResolvers() as IResolvers, createEventsResolvers(libs) as IResolvers, createHostsResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts deleted file mode 100644 index dafbccae09c0e1..00000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import axios from 'axios'; -import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; -import { CaseSavedObject, CasesSavedObjects, PageInfoCase, SortCase } from '../../graphql/types'; -import { FrameworkRequest } from '../framework'; -import { caseSavedObjectType } from './saved_object_mappings'; - -export class Case { - public async deleteCase(request: FrameworkRequest, noteIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - noteIds.map(noteId => savedObjectsClient.delete(caseSavedObjectType, noteId)) - ); - } - - public async getCase(request: FrameworkRequest, caseId: string): Promise { - return this.getSavedCase(request, caseId); - } - - public async getCases( - request: FrameworkRequest, - pageInfo: PageInfoCase | null, - search: string | null, - sort: SortCase | null - ): Promise { - const options: SavedObjectsFindOptions = { - type: caseSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex + 1 : undefined, // + 1 because table pagination starts at 0 and saved object page index starts at 1 - search: search != null ? search : undefined, - searchFields: ['tags'], - sortField: sort != null ? sort.field : undefined, - sortOrder: sort != null ? sort.direction : undefined, - }; - - return this.getAllSavedCase(request, options); - } - - private async getSavedCase(request: FrameworkRequest, caseId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(caseSavedObjectType, caseId); - // - // 'SAVED CASE!!!', - // JSON.stringify({ - // caseSavedObjectType, - // caseId, - // savedObject, - // }) - // ); - - return convertSavedObjectToSavedCase(savedObject); - } - - private async getAllSavedCase(request: FrameworkRequest, options: SavedObjectsFindOptions) { - // const savedObjectsClient = request.context.core.savedObjects.client; - console.log('saved objects find options', options); - let response; - try { - response = await axios.get('/api/cases', { - params: options, - }); - console.log(response); - } catch (error) { - console.error(error); - return error; - } - const { data }: { data: CasesSavedObjects } = response; // await savedObjectsClient.find(options); - return { - ...data, - saved_objects: data.saved_objects.map(savedObject => - convertSavedObjectToSavedCase(savedObject) - ), - }; - } -} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const convertSavedObjectToSavedCase = (savedObject: any): any => savedObject; - -// ( -// savedObject: unknown, -// timelineVersion?: string | undefined | null -// ): CaseSavedObject => -// pipe( -// CaseSavedObjectRuntimeType.decode(savedObject), -// map(savedCase => ({ -// noteId: savedCase.id, -// version: savedCase.version, -// timelineVersion, -// ...savedCase.attributes, -// })), -// fold(errors => { -// throw new Error(failure(errors).join('\n')); -// }, identity) -// ); - -// we have to use any here because the SavedObjectAttributes interface is like below -// export interface SavedObjectAttributes { -// [key: string]: SavedObjectAttributes | string | number | boolean | null; -// } -// then this interface does not allow types without index signature -// this is limiting us with our type for now so the easy way was to use any -// -// const pickSavedCase = ( -// noteId: string | null, -// savedCase: SavedCase, -// userInfo: AuthenticatedUser | null -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// ): any => { -// if (noteId == null) { -// savedCase.created = new Date().valueOf(); -// savedCase.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; -// savedCase.updated = new Date().valueOf(); -// savedCase.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; -// } else if (noteId != null) { -// savedCase.updated = new Date().valueOf(); -// savedCase.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; -// } -// return savedCase; -// }; diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index db4a769c54cd9a..30fdf7520a3ed1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -30,7 +30,6 @@ import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes'; -import { Case } from '../case/saved_object'; import { Note } from '../note/saved_object'; import { PinnedEvent } from '../pinned_event/saved_object'; import { Timeline } from '../timeline/saved_object'; @@ -48,7 +47,6 @@ export function compose( const timeline = new Timeline(); const note = new Note(); const pinnedEvent = new PinnedEvent(); - const caseWorkflow = new Case(); const domainLibs: AppDomainLibs = { alerts: new Alerts(new ElasticsearchAlertsAdapter(framework)), @@ -74,7 +72,6 @@ export function compose( timeline, note, pinnedEvent, - case: caseWorkflow, }; return libs; diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index fc50969add08d5..9034ab4e6af83d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -7,7 +7,6 @@ export { ConfigType as Configuration } from '../../../../../plugins/siem/server'; import { Anomalies } from './anomalies'; import { Authentications } from './authentications'; -import { Case } from './case/saved_object'; import { Events } from './events'; import { FrameworkAdapter, FrameworkRequest } from './framework'; import { Hosts } from './hosts'; @@ -45,7 +44,6 @@ export interface AppDomainLibs { } export interface AppBackendLibs extends AppDomainLibs { - case: Case; framework: FrameworkAdapter; sources: Sources; sourceStatus: SourceStatus; From 834770aa12dad532e38ca288d77ed5432dddd488 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 22 Jan 2020 11:48:32 -0700 Subject: [PATCH 15/67] more refactor --- .../case => components/case/api}/constants.ts | 2 ++ .../case/api}/use_get_case.tsx | 2 +- .../case/api}/use_get_cases.tsx | 24 +++++++++++------ .../{hooks => components}/case/types.ts | 27 ++++--------------- .../case => case/views}/case_view/index.tsx | 6 ++--- .../views}/cases_table/columns.tsx | 0 .../views/cases_table/index.tsx} | 8 +++--- .../views}/cases_table/translations.ts | 0 .../page/case/cases_table/index.tsx | 19 ------------- .../plugins/siem/public/pages/case/case.tsx | 6 ++--- .../siem/public/pages/case/case_details.tsx | 2 +- .../plugins/siem/public/pages/case/index.tsx | 4 +-- 12 files changed, 37 insertions(+), 63 deletions(-) rename x-pack/legacy/plugins/siem/public/{hooks/case => components/case/api}/constants.ts (83%) rename x-pack/legacy/plugins/siem/public/{hooks/case => components/case/api}/use_get_case.tsx (98%) rename x-pack/legacy/plugins/siem/public/{hooks/case => components/case/api}/use_get_cases.tsx (86%) rename x-pack/legacy/plugins/siem/public/{hooks => components}/case/types.ts (85%) rename x-pack/legacy/plugins/siem/public/components/{page/case => case/views}/case_view/index.tsx (94%) rename x-pack/legacy/plugins/siem/public/components/{page/case => case/views}/cases_table/columns.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{page/case/cases_table/table.tsx => case/views/cases_table/index.tsx} (90%) rename x-pack/legacy/plugins/siem/public/components/{page/case => case/views}/cases_table/translations.ts (100%) delete mode 100644 x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/constants.ts b/x-pack/legacy/plugins/siem/public/components/case/api/constants.ts similarity index 83% rename from x-pack/legacy/plugins/siem/public/hooks/case/constants.ts rename to x-pack/legacy/plugins/siem/public/components/case/api/constants.ts index f9e6037a9efb6c..5dc45a82c6ef61 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/case/api/constants.ts @@ -8,3 +8,5 @@ export const FETCH_INIT = 'FETCH_INIT'; export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const FETCH_FAILURE = 'FETCH_FAILURE'; export const UPDATE_TABLE = 'UPDATE_TABLE'; +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/components/case/api/use_get_case.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx rename to x-pack/legacy/plugins/siem/public/components/case/api/use_get_case.tsx index fa9f14c9726c7c..b4fccabb01a8e0 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/components/case/api/use_get_case.tsx @@ -7,7 +7,7 @@ import { useEffect, useReducer, useState } from 'react'; import chrome from 'ui/chrome'; -import { CaseSavedObject } from './types'; +import { CaseSavedObject } from '../types'; import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; interface CaseState { diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/components/case/api/use_get_cases.tsx similarity index 86% rename from x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx rename to x-pack/legacy/plugins/siem/public/components/case/api/use_get_cases.tsx index 2645cd5cd3ad8b..3e01a3663ce4be 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/components/case/api/use_get_cases.tsx @@ -7,9 +7,15 @@ import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; import chrome from 'ui/chrome'; -import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS, UPDATE_TABLE } from './constants'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; -import { CasesSavedObjects, Direction, SortFieldCase } from './types'; +import { + DEFAULT_TABLE_ACTIVE_PAGE, + DEFAULT_TABLE_LIMIT, + FETCH_FAILURE, + FETCH_INIT, + FETCH_SUCCESS, + UPDATE_TABLE, +} from './constants'; +import { CasesSavedObjects, Direction, SortFieldCase } from '../types'; interface TableArgs { page: number; @@ -102,9 +108,10 @@ export const useGetCases = (): [CasesState, Dispatch>] const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const queryParams = Object.entries(state.table).reduce((acc, [key, value]) => { - return `${acc}${key}=${value}&`; - }, '?'); + const queryParams = Object.entries(state.table).reduce( + (acc, [key, value]) => `${acc}${key}=${value}&`, + '?' + ); const result = await fetch(`${chrome.getBasePath()}/api/cases${queryParams}`, { method: 'GET', credentials: 'same-origin', @@ -117,9 +124,10 @@ export const useGetCases = (): [CasesState, Dispatch>] if (!didCancel) { const resultJson = await result.json(); if (resultJson.statusCode >= 400) { - return dispatch({ type: FETCH_FAILURE }); + dispatch({ type: FETCH_FAILURE }); + } else { + dispatch({ type: FETCH_SUCCESS, payload: resultJson }); } - dispatch({ type: FETCH_SUCCESS, payload: resultJson }); } } catch (error) { if (!didCancel) { diff --git a/x-pack/legacy/plugins/siem/public/hooks/case/types.ts b/x-pack/legacy/plugins/siem/public/components/case/types.ts similarity index 85% rename from x-pack/legacy/plugins/siem/public/hooks/case/types.ts rename to x-pack/legacy/plugins/siem/public/components/case/types.ts index 713d0bd8d24157..2f35f1f0478fa7 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/case/types.ts @@ -5,48 +5,31 @@ */ import { SavedObjectsBaseOptions } from 'kibana/server'; -import { Direction, Maybe } from '../../graphql/types'; -export { Direction }; -export interface CasesSavedObjects { - saved_objects: Array>; +export { Direction } from '../../graphql/types'; +export interface CasesSavedObjects { + saved_objects: CaseSavedObject[] | []; page: number; - per_page: number; - total: number; } export interface CaseSavedObject { attributes: CaseResult; - id: string; - type: string; - updated_at: string; - version: string; } export interface CaseResult { case_type: string; - created_at: number; - created_by: ElasticUser; - description: string; - state: string; - - tags: Array>; - + tags: string[] | []; title: string; } -export interface SortCase { - field: SortFieldCase; - direction: Direction; -} export enum SortFieldCase { createdAt = 'created_at', state = 'state', @@ -74,5 +57,5 @@ export interface CaseFindOptions extends SavedObjectsBaseOptions { export interface ElasticUser { username: string; - full_name?: Maybe; + full_name?: string; } diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/components/case/views/case_view/index.tsx similarity index 94% rename from x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx rename to x-pack/legacy/plugins/siem/public/components/case/views/case_view/index.tsx index 2ece312c4641ba..af2fe6ee376e43 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/case/views/case_view/index.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { HeaderPage } from '../../../header_page'; import * as i18n from '../../../../pages/case/translations'; import { getCaseUrl } from '../../../link_to'; -import { useGetCase } from '../../../../hooks/case/use_get_case'; +import { useGetCase } from '../../api/use_get_case'; interface Props { caseId: string; @@ -64,8 +64,8 @@ export const CaseView = React.memo(({ caseId }: Props) => { definition: data.attributes.tags.length > 0 ? (
    - {data.attributes.tags.map((tag, key) => ( -
  • {tag}
  • + {data.attributes.tags.map((tag: string, key: number) => ( +
  • {tag}
  • ))}
) : null, diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/case/cases_table/columns.tsx rename to x-pack/legacy/plugins/siem/public/components/case/views/cases_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx b/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx similarity index 90% rename from x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx rename to x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx index c38124baabf275..a6767a6226a321 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx @@ -10,8 +10,8 @@ import * as i18n from './translations'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; import { getCasesColumns } from './columns'; -import { Direction, SortFieldCase } from '../../../../hooks/case/types'; -import { useGetCases } from '../../../../hooks/case/use_get_cases'; +import { Direction, SortFieldCase } from '../../types'; +import { useGetCases } from '../../api/use_get_cases'; const rowItems: ItemsPerRow[] = [ { @@ -23,7 +23,7 @@ const rowItems: ItemsPerRow[] = [ numberOfRow: 10, }, ]; -export const CasesPaginatedTable = React.memo(() => { +export const CasesTable = React.memo(() => { const [ { data, @@ -96,4 +96,4 @@ export const CasesPaginatedTable = React.memo(() => { ); }); -CasesPaginatedTable.displayName = 'CasesPaginatedTable'; +CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/case/cases_table/translations.ts rename to x-pack/legacy/plugins/siem/public/components/case/views/cases_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx deleted file mode 100644 index a11be19ded03b2..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/case/cases_table/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiFlexItem } from '@elastic/eui'; -import { CasesPaginatedTable } from './table'; - -export const CasesTable = React.memo(() => { - return ( - - - - ); -}); - -CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 8c617237d792f4..c1fa8ae8034835 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -13,13 +13,13 @@ import { EmptyPage } from '../../components/empty_page'; import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CasesTable } from '../../components/page/case/cases_table'; +import { CasesTable } from '../../components/case/views/cases_table'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; const basePath = chrome.getBasePath(); -export const CaseComponent = React.memo(() => { +export const CasePage = React.memo(() => { const docLinks = useKibana().services.docLinks; return ( @@ -62,4 +62,4 @@ export const CaseComponent = React.memo(() => { ); }); -CaseComponent.displayName = 'CaseComponent'; +CasePage.displayName = 'CasePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index 1123cc7f8860fb..f4215d7c8d8571 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../lib/kibana'; import { EmptyPage } from '../../components/empty_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CaseView } from '../../components/page/case/case_view'; +import { CaseView } from '../../components/case/views/case_view'; import * as i18n from './translations'; import { SpyRoute } from '../../utils/route/spy_routes'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx index 28e44cc06cbb5c..3f80efc3d72da8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Route, Switch, RouteComponentProps } from 'react-router-dom'; import { SiemPageName } from '../home/types'; -import { CaseComponent } from './case'; +import { CasePage } from './case'; import { CaseDetails } from './case_details'; type Props = Partial> & { url: string }; @@ -19,7 +19,7 @@ const caseDetailsPagePath = `${casesPagePath}/:detailName`; const CaseContainerComponent: React.FC = () => { return ( - } /> + } /> Date: Wed, 22 Jan 2020 14:32:45 -0700 Subject: [PATCH 16/67] search bar implementation --- .../case/views/cases_table/index.tsx | 57 ++-- .../components/case/views/search_bar/data.ts | 96 ++++++ .../case/views/search_bar/index.tsx | 308 ++++++++++++++++++ 3 files changed, 438 insertions(+), 23 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/case/views/search_bar/data.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/case/views/search_bar/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx index a6767a6226a321..1e3d66694ede1e 100644 --- a/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx @@ -6,12 +6,14 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import * as i18n from './translations'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; import { getCasesColumns } from './columns'; import { Direction, SortFieldCase } from '../../types'; import { useGetCases } from '../../api/use_get_cases'; +import { CasesSearchBar } from '../search_bar'; const rowItems: ItemsPerRow[] = [ { @@ -70,29 +72,38 @@ export const CasesTable = React.memo(() => { const sorting = { field: `attributes.${sortField}`, direction: sortOrder }; return isError ? null : ( - - } - headerUnit={i18n.UNIT(data.total)} - hideInspect={true} - id={'getCasesTable'} - itemsPerRow={rowItems} - limit={perPage} - limitResetsActivePage={false} - loading={isLoading} - loadPage={newPage => newPage} - onChange={onChange} - pageOfItems={data.saved_objects} - showMorePagesIndicator={false} - sorting={sorting} - totalCount={data.total} - updateActivePage={updateActivePage} - updateLimitPagination={updateLimitPagination} - /> + <> + + + + + + + } + headerUnit={i18n.UNIT(data.total)} + hideInspect={true} + id={'getCasesTable'} + itemsPerRow={rowItems} + limit={perPage} + limitResetsActivePage={false} + loading={isLoading} + loadPage={newPage => newPage} + onChange={onChange} + pageOfItems={data.saved_objects} + showMorePagesIndicator={false} + sorting={sorting} + totalCount={data.total} + updateActivePage={updateActivePage} + updateLimitPagination={updateLimitPagination} + /> + + + ); }); diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/data.ts b/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/data.ts new file mode 100644 index 00000000000000..7636cecdb952d7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/data.ts @@ -0,0 +1,96 @@ +/* + * 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. + */ + +export const data = [ + { + type: 'case-workflow', + id: '058cbbd0-20da-11ea-b554-4dead8f3ebd9', + attributes: { + created_at: 1576593135435, + created_by: { full_name: null, username: 'elastic' }, + description: 'Urgent phishing case', + title: 'Suspicious email attachment opened', + state: 'open', + tags: ['phishing'], + case_type: 'security', + assignees: [ + { username: 'M', full_name: 'Classified' }, + { username: '007', full_name: 'James Bond' }, + ], + }, + references: [], + updated_at: '2019-12-17T14:32:15.629Z', + version: 'WzIxLDFd', + }, + { + type: 'case-workflow', + id: '335bf230-20d8-11ea-b554-4dead8f3ebd9', + attributes: { + created_at: 1576592353300, + created_by: { full_name: null, username: 'elastic' }, + title: 'Super Bad Security Issue', + description: 'bubblegum attack', + case_type: '', + assignees: [], + state: 'open', + tags: [], + }, + references: [], + updated_at: '2019-12-17T14:19:13.491Z', + version: 'WzIwLDFd', + }, + { + type: 'case-workflow', + id: '14583010-20d8-11ea-b554-4dead8f3ebd9', + attributes: { + created_at: 1576592301271, + created_by: { full_name: null, username: 'elastic' }, + title: 'Super Bad Security Issue', + description: 'bubblegum attack', + case_type: '', + assignees: [], + state: 'open', + tags: [], + }, + references: [], + updated_at: '2019-12-17T14:18:21.457Z', + version: 'WzE5LDFd', + }, + { + type: 'case-workflow', + id: 'bf9b5d80-20d3-11ea-b554-4dead8f3ebd9', + attributes: { + created_at: 1576590441102, + created_by: { full_name: null, username: 'elastic' }, + description: 'Urgent phishing case', + title: 'Suspicious email attachment opened', + state: 'open', + tags: ['phishing'], + case_type: 'security', + assignees: [{ username: 'Q' }], + }, + references: [], + updated_at: '2019-12-17T13:47:21.304Z', + version: 'WzE2LDFd', + }, + { + type: 'case-workflow', + id: '5cf3c4a0-20cf-11ea-b554-4dead8f3ebd9', + attributes: { + created_at: 1576588557506, + created_by: { full_name: null, username: 'elastic' }, + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + state: 'open', + tags: ['defacement'], + case_type: 'security', + assignees: [], + }, + references: [], + updated_at: '2019-12-17T13:15:57.802Z', + version: 'WzEzLDFd', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/index.tsx new file mode 100644 index 00000000000000..55b493e33412d9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/index.tsx @@ -0,0 +1,308 @@ +/* + * 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 } from 'react'; +import { + EuiHealth, + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiBasicTable, + EuiSearchBar, + EuiButton, +} from '@elastic/eui'; + +const tags = [ + { name: 'marketing', color: 'danger' }, + { name: 'finance', color: 'success' }, + { name: 'eng', color: 'success' }, + { name: 'sales', color: 'warning' }, + { name: 'ga', color: 'success' }, +]; + +const types = ['dashboard', 'visualization', 'watch']; + +const users = ['dewey', 'wanda', 'carrie', 'jmack', 'gabic']; + +const items = [ + { + id: 100001, + status: 'open', + type: types[0], + tag: [tags[0].name, tags[1].name, tags[2].name], + active: true, + owner: users[0], + followers: 19, + comments: 7, + stars: 3, + }, + { + id: 100002, + status: 'open', + type: types[1], + tag: [tags[1].name, tags[4].name, tags[2].name], + active: true, + owner: users[2], + followers: 15, + comments: 6, + stars: 2, + }, + { + id: 100003, + status: 'closed', + type: types[0], + tag: [tags[2].name], + active: true, + owner: users[3], + followers: 11, + comments: 3, + stars: 1, + }, + { + id: 100004, + status: 'open', + type: types[2], + tag: [tags[2].name, tags[3].name], + active: false, + owner: users[2], + followers: 14, + comments: 8, + stars: 4, + }, +]; + +const loadTags = () => { + return new Promise(resolve => { + setTimeout(() => { + resolve( + tags.map(tag => ({ + value: tag.name, + view: {tag.name}, + })) + ); + }, 2000); + }); +}; + +const initialQuery = EuiSearchBar.Query.MATCH_ALL; + +export const CasesSearchBar = React.memo(() => { + const [currentQuery, setQuery] = useState(initialQuery); + const [currentError, setError] = useState(null); + const [incremental, setIncremental] = useState(false); + + const onChange = ({ query, error }) => { + if (error) { + setError(error); + } else { + setError(null); + setQuery(query); + } + }; + + const toggleIncremental = () => setIncremental(!incremental); + + const renderBookmarks = () => ( + <> +

{`Enter a query, or select one from a bookmark`}

+ + + + setQuery('status:open owner:dewey')}> + {`mine, open`} + + + + setQuery('status:closed owner:dewey')}> + {`mine, closed`} + + + + + + ); + + const renderSearch = () => { + const filters = [ + { + type: 'field_value_toggle_group', + field: 'status', + items: [ + { + value: 'open', + name: 'Open', + }, + { + value: 'closed', + name: 'Closed', + }, + ], + }, + { + type: 'is', + field: 'active', + name: 'Active', + negatedName: 'Inactive', + }, + { + type: 'field_value_toggle', + name: 'Mine', + field: 'owner', + value: 'dewey', + }, + { + type: 'field_value_selection', + field: 'tag', + name: 'Tag', + multiSelect: 'or', + cache: 10000, // will cache the loaded tags for 10 sec + options: () => loadTags(), + }, + ]; + const schema = { + strict: true, + fields: { + active: { + type: 'boolean', + }, + status: { + type: 'string', + }, + followers: { + type: 'number', + }, + comments: { + type: 'number', + }, + stars: { + type: 'number', + }, + created: { + type: 'date', + }, + owner: { + type: 'string', + }, + tag: { + type: 'string', + validate: value => { + if (!tags.some(tag => tag.name === value)) { + throw new Error( + `unknown tag (possible values: ${tags.map(tag => tag.name).join(',')})` + ); + } + }, + }, + }, + }; + return ( + + ); + }; + + const renderError = () => { + if (!currentError) { + return; + } + return ( + <> + + + + ); + }; + + const renderTable = () => { + const columns = [ + { + name: 'Type', + field: 'type', + }, + { + name: 'Open', + field: 'status', + render: status => (status === 'open' ? 'Yes' : 'No'), + }, + { + name: 'Active', + field: 'active', + dataType: 'boolean', + }, + { + name: 'Tags', + field: 'tag', + render: itemTags => + itemTags.map((tag: string, key: number) => + key + 1 < itemTags.length ? `${tag}, ` : tag + ), + }, + { + name: 'Owner', + field: 'owner', + }, + { + name: 'Stats', + width: '150px', + render: item => { + return ( +
+
{`${item.stars} Stars`}
+
{`${item.followers} Followers`}
+
{`${item.comments} Comments`}
+
+ ); + }, + }, + ]; + + const queriedItems = EuiSearchBar.Query.execute(currentQuery, items, { + defaultFields: ['owner', 'tag', 'type'], + }); + console.log('queriedItems', queriedItems); + + return ; + }; + + const content = renderError() || ( + + {renderTable()} + + ); + + return ( + <> + + {renderBookmarks()} + + + {renderSearch()} + + + + + + + {content} + + ); +}); + +CasesSearchBar.displayName = 'CasesSearchBar'; From b3e3ba148977d1a618e531ffef42d812d288e960 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 23 Jan 2020 12:20:24 -0700 Subject: [PATCH 17/67] reorg dirs --- .../case/api => containers/case}/constants.ts | 0 .../public/{components => containers}/case/types.ts | 0 .../case/api => containers/case}/use_get_case.tsx | 2 +- .../case/api => containers/case}/use_get_cases.tsx | 2 +- x-pack/legacy/plugins/siem/public/pages/case/case.tsx | 2 +- .../plugins/siem/public/pages/case/case_details.tsx | 2 +- .../case/components}/case_view/index.tsx | 8 ++++---- .../case/components}/cases_table/columns.tsx | 10 +++++----- .../case/components}/cases_table/index.tsx | 8 ++++---- .../case/components}/cases_table/translations.ts | 0 .../views => pages/case/components}/search_bar/data.ts | 0 .../case/components}/search_bar/index.tsx | 0 .../plugins/siem/public/pages/case/translations.ts | 1 + 13 files changed, 18 insertions(+), 17 deletions(-) rename x-pack/legacy/plugins/siem/public/{components/case/api => containers/case}/constants.ts (100%) rename x-pack/legacy/plugins/siem/public/{components => containers}/case/types.ts (100%) rename x-pack/legacy/plugins/siem/public/{components/case/api => containers/case}/use_get_case.tsx (98%) rename x-pack/legacy/plugins/siem/public/{components/case/api => containers/case}/use_get_cases.tsx (97%) rename x-pack/legacy/plugins/siem/public/{components/case/views => pages/case/components}/case_view/index.tsx (92%) rename x-pack/legacy/plugins/siem/public/{components/case/views => pages/case/components}/cases_table/columns.tsx (83%) rename x-pack/legacy/plugins/siem/public/{components/case/views => pages/case/components}/cases_table/index.tsx (92%) rename x-pack/legacy/plugins/siem/public/{components/case/views => pages/case/components}/cases_table/translations.ts (100%) rename x-pack/legacy/plugins/siem/public/{components/case/views => pages/case/components}/search_bar/data.ts (100%) rename x-pack/legacy/plugins/siem/public/{components/case/views => pages/case/components}/search_bar/index.tsx (100%) diff --git a/x-pack/legacy/plugins/siem/public/components/case/api/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/case/api/constants.ts rename to x-pack/legacy/plugins/siem/public/containers/case/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/case/types.ts rename to x-pack/legacy/plugins/siem/public/containers/case/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/case/api/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/case/api/use_get_case.tsx rename to x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index b4fccabb01a8e0..fa9f14c9726c7c 100644 --- a/x-pack/legacy/plugins/siem/public/components/case/api/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -7,7 +7,7 @@ import { useEffect, useReducer, useState } from 'react'; import chrome from 'ui/chrome'; -import { CaseSavedObject } from '../types'; +import { CaseSavedObject } from './types'; import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; interface CaseState { diff --git a/x-pack/legacy/plugins/siem/public/components/case/api/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/case/api/use_get_cases.tsx rename to x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 3e01a3663ce4be..9aeb4c01e01fc1 100644 --- a/x-pack/legacy/plugins/siem/public/components/case/api/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -15,7 +15,7 @@ import { FETCH_SUCCESS, UPDATE_TABLE, } from './constants'; -import { CasesSavedObjects, Direction, SortFieldCase } from '../types'; +import { CasesSavedObjects, Direction, SortFieldCase } from './types'; interface TableArgs { page: number; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index c1fa8ae8034835..c0dcfd7036b063 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -13,7 +13,7 @@ import { EmptyPage } from '../../components/empty_page'; import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CasesTable } from '../../components/case/views/cases_table'; +import { CasesTable } from './cases_table'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index f4215d7c8d8571..5d9701336d0e84 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../lib/kibana'; import { EmptyPage } from '../../components/empty_page'; import { WrapperPage } from '../../components/wrapper_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CaseView } from '../../components/case/views/case_view'; +import { CaseView } from './case_view'; import * as i18n from './translations'; import { SpyRoute } from '../../utils/route/spy_routes'; diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/case/views/case_view/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index af2fe6ee376e43..14241fbed7f7b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/case/views/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -8,10 +8,10 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { HeaderPage } from '../../../header_page'; -import * as i18n from '../../../../pages/case/translations'; -import { getCaseUrl } from '../../../link_to'; -import { useGetCase } from '../../api/use_get_case'; +import { HeaderPage } from '../../../../components/header_page'; +import * as i18n from '../../translations'; +import { getCaseUrl } from '../../../../components/link_to'; +import { useGetCase } from '../../../../containers/case/use_get_case'; interface Props { caseId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/columns.tsx similarity index 83% rename from x-pack/legacy/plugins/siem/public/components/case/views/cases_table/columns.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/columns.tsx index 3a89d12441ab78..008b2df63b6ff4 100644 --- a/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/columns.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { getEmptyTagValue } from '../../../empty_value'; -import { Columns } from '../../../paginated_table'; -import { CaseSavedObject } from '../../../../hooks/case/types'; -import { FormattedRelativePreferenceDate } from '../../../formatted_date'; -import { CaseDetailsLink } from '../../../links'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { Columns } from '../../../../components/paginated_table'; +import { CaseSavedObject } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { CaseDetailsLink } from '../../../../components/links'; export type CasesColumns = [ Columns, diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/index.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/index.tsx index 1e3d66694ede1e..d1c2f7f64063e4 100644 --- a/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/index.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import * as i18n from './translations'; -import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../../components/paginated_table'; import { getCasesColumns } from './columns'; -import { Direction, SortFieldCase } from '../../types'; -import { useGetCases } from '../../api/use_get_cases'; -import { CasesSearchBar } from '../search_bar'; +import { Direction, SortFieldCase } from '../../../../containers/case/types'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { CasesSearchBar } from '../../search_bar'; const rowItems: ItemsPerRow[] = [ { diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/cases_table/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/case/views/cases_table/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/data.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/case/views/search_bar/data.ts rename to x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/data.ts diff --git a/x-pack/legacy/plugins/siem/public/components/case/views/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/case/views/search_bar/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 480518e436800d..c6c45479e44476 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -38,3 +38,4 @@ export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.case.emptyActionP export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.case.emptyActionSecondary', { defaultMessage: 'Go to documentation', }); + From 0e6cce1fed1608c3259443808f467ac35fd1a8fe Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 23 Jan 2020 14:58:50 -0700 Subject: [PATCH 18/67] basic table --- .../components/paginated_table/index.tsx | 2 +- .../public/containers/case/use_get_cases.tsx | 18 +-- .../plugins/siem/public/pages/case/case.tsx | 70 +++------- .../siem/public/pages/case/case_details.tsx | 52 ++------ .../{cases_table => all_cases}/columns.tsx | 46 ++++--- .../pages/case/components/all_cases/index.tsx | 126 ++++++++++++++++++ .../case/components/all_cases/translations.ts | 60 +++++++++ .../case/components/cases_table/index.tsx | 110 --------------- .../components/cases_table/translations.ts | 27 ---- .../plugins/siem/public/pages/case/index.tsx | 9 +- .../siem/public/pages/case/translations.ts | 1 - 11 files changed, 261 insertions(+), 260 deletions(-) rename x-pack/legacy/plugins/siem/public/pages/case/components/{cases_table => all_cases}/columns.tsx (63%) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/index.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index fb17ba409ff516..e726803cdd5270 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -22,7 +22,7 @@ import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; import styled from 'styled-components'; import { AuthTableColumns } from '../page/hosts/authentications_table'; -import { CasesColumns } from '../page/case/cases_table/columns'; +import { CasesColumns } from '../page/case/all_cases/columns'; import { HostsTableColumns } from '../page/hosts/hosts_table'; import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 9aeb4c01e01fc1..f5f1f2f61834ea 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -17,7 +17,7 @@ import { } from './constants'; import { CasesSavedObjects, Direction, SortFieldCase } from './types'; -interface TableArgs { +interface PaginationArgs { page: number; perPage: number; sortField: SortFieldCase; @@ -35,7 +35,7 @@ interface CasesState { data: CasesSavedObjects; isLoading: boolean; isError: boolean; - table: TableArgs; + pagination: PaginationArgs; } interface PayloadObj { [key: string]: unknown; @@ -70,8 +70,8 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { case UPDATE_TABLE: return { ...state, - table: { - ...state.table, + pagination: { + ...state.pagination, ...action.payload, }, }; @@ -90,14 +90,14 @@ export const useGetCases = (): [CasesState, Dispatch>] isLoading: false, isError: false, data: initialData, - table: { - page: DEFAULT_TABLE_ACTIVE_PAGE + 1, + pagination: { + page: DEFAULT_TABLE_ACTIVE_PAGE, perPage: DEFAULT_TABLE_LIMIT, sortField: SortFieldCase.createdAt, sortOrder: Direction.desc, }, }); - const [query, setQuery] = useState(state.table as QueryArgs); + const [query, setQuery] = useState(state.pagination as QueryArgs); useEffect(() => { dispatch({ type: UPDATE_TABLE, payload: query }); @@ -108,7 +108,7 @@ export const useGetCases = (): [CasesState, Dispatch>] const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const queryParams = Object.entries(state.table).reduce( + const queryParams = Object.entries(state.pagination).reduce( (acc, [key, value]) => `${acc}${key}=${value}&`, '?' ); @@ -139,6 +139,6 @@ export const useGetCases = (): [CasesState, Dispatch>] return () => { didCancel = true; }; - }, [state.table]); + }, [state.pagination]); return [state, setQuery]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index c0dcfd7036b063..0ca9356dfa482a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -4,62 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; -import chrome from 'ui/chrome'; -import { useKibana } from '../../lib/kibana'; -import { EmptyPage } from '../../components/empty_page'; import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CasesTable } from './cases_table'; +import { AllCases } from './components/all_cases'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; -const basePath = chrome.getBasePath(); +export const CasesPage = React.memo(() => ( + <> + + + + + + +)); -export const CasePage = React.memo(() => { - const docLinks = useKibana().services.docLinks; - - return ( - <> - - - - - {({ indicesExist }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - ) : ( - - ) - } - - - - - ); -}); -CasePage.displayName = 'CasePage'; +CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index 5d9701336d0e84..f12d996ab49ad5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -6,52 +6,24 @@ import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; -import chrome from 'ui/chrome'; -import { useKibana } from '../../lib/kibana'; -import { EmptyPage } from '../../components/empty_page'; import { WrapperPage } from '../../components/wrapper_page'; -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { CaseView } from './case_view'; -import * as i18n from './translations'; +import { CaseView } from './components/case_view'; import { SpyRoute } from '../../utils/route/spy_routes'; -const basePath = chrome.getBasePath(); - interface Props { caseId: string; } -export const CaseDetails = React.memo(({ caseId }: Props) => { - const docLinks = useKibana().services.docLinks; - return ( - <> - - {({ indicesExist }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - ) : ( - - ) - } - - - - ); -}); +export const CaseDetailsPage = React.memo(({ caseId }: Props) => ( + <> + + + + + + + +)); -CaseDetails.displayName = 'CaseDetails'; +CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx similarity index 63% rename from x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/columns.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 008b2df63b6ff4..13598b4ffbc848 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -4,32 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { EuiBadge, EuiTableFieldDataColumnType } from '@elastic/eui'; import { getEmptyTagValue } from '../../../../components/empty_value'; -import { Columns } from '../../../../components/paginated_table'; import { CaseSavedObject } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; import { CaseDetailsLink } from '../../../../components/links'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import * as i18n from './translations'; -export type CasesColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; +export type CasesColumns = EuiTableFieldDataColumnType; const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); -export const getCasesColumns = (): CasesColumns => [ +export const getCasesColumns = (): CasesColumns[] => [ { field: 'attributes.title', - name: 'Case Title', + name: i18n.CASE_TITLE, render: title => renderStringField(title), }, { field: 'id', - name: 'Case Id', + name: i18n.CASE_ID, render: id => { if (id != null) { return ; @@ -37,9 +32,28 @@ export const getCasesColumns = (): CasesColumns => [ return getEmptyTagValue(); }, }, + { + field: 'attributes.tags', + name: i18n.TAGS, + render: tags => { + if (tags != null && tags.length > 0) { + return ( + + {tags.map((tag, i) => ( + + {tag} + + ))} + + ); + } + return getEmptyTagValue(); + }, + truncateText: true, + }, { field: 'attributes.created_at', - name: 'Created at', + name: i18n.CREATED_AT, sortable: true, render: createdAt => { if (createdAt != null) { @@ -50,12 +64,12 @@ export const getCasesColumns = (): CasesColumns => [ }, { field: 'attributes.created_by.username', - name: 'Created by', + name: i18n.CREATED_BY, render: createdBy => renderStringField(createdBy), }, { field: 'updated_at', - name: 'Last updated', + name: i18n.LAST_UPDATED, sortable: true, render: updatedAt => { if (updatedAt != null) { @@ -66,7 +80,7 @@ export const getCasesColumns = (): CasesColumns => [ }, { field: 'attributes.state', - name: 'State', + name: i18n.STATE, sortable: true, render: state => renderStringField(state), }, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx new file mode 100644 index 00000000000000..70e47c446fa2d5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -0,0 +1,126 @@ +/* + * 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, { useCallback } from 'react'; +import { + EuiBasicTable, + EuiButton, + EuiEmptyPrompt, + EuiLoadingContent, + EuiTableSortingType, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import * as i18n from './translations'; + +import { getCasesColumns } from './columns'; +import { Direction, SortFieldCase, CaseSavedObject } from '../../../../containers/case/types'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; +import { Panel } from '../../../../components/panel'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; + +export const AllCases = React.memo(() => { + const [{ data, isLoading, isError, pagination }, doFetch] = useGetCases(); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + let newPagination = {}; + if (sort) { + let newSort; + switch (sort.field) { + case 'attributes.state': + newSort = SortFieldCase.state; + break; + case 'attributes.created_at': + newSort = 'attributes.created_at'; + break; + case 'updated_at': + newSort = SortFieldCase.updatedAt; + break; + default: + newSort = SortFieldCase.createdAt; + } + const oppositeSort = sort.direction === Direction.asc ? Direction.desc : Direction.asc; + const newOrder = + sort.direction === pagination.sortOrder && pagination.sortField === newSort + ? oppositeSort + : sort.direction; + newPagination = { + ...newPagination, + sortField: newSort, + sortOrder: newOrder as Direction, + }; + } + if (page) { + newPagination = { + ...newPagination, + page: page.index + 1, + perPage: page.size, + }; + } + doFetch(newPagination); + }, + [doFetch, pagination] + ); + + const sorting = { + sort: { field: pagination.sortField, direction: pagination.sortOrder }, + } as EuiTableSortingType; + return isError ? null : ( + + +

{`TableFilters placeholder`}

+
+ {isLoading && isEmpty(data.saved_objects) && ( + + )} + {!isLoading && !isEmpty(data.saved_objects) && ( + <> + + + + {i18n.SHOWING_CASES(data.total ?? 0)} + + + + {i18n.NO_CASES}} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + + {i18n.ADD_NEW_CASE} + + } + /> + } + onChange={tableOnChangeCallback} + pagination={{ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: data.total, + pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + }} + sorting={sorting} + /> + + )} +
+ ); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts new file mode 100644 index 00000000000000..de8a4bde7bbf14 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -0,0 +1,60 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ALL_CASES = i18n.translate('xpack.siem.caseTable.title', { + defaultMessage: 'All Cases', +}); +export const NO_CASES = i18n.translate('xpack.siem.caseTable.noCases.title', { + defaultMessage: 'No Cases', +}); +export const NO_CASES_BODY = i18n.translate('xpack.siem.caseTable.noCases.body', { + defaultMessage: 'Create a new case to see it displayed in the case workflow table.', +}); +export const ADD_NEW_CASE = i18n.translate('xpack.siem.caseTable.addNewCase', { + defaultMessage: 'Add New Case', +}); + +export const SHOWING_CASES = (totalRules: number) => + i18n.translate('xpack.siem.caseTable.showingCasesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.caseTable.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, + }); + +export const CASE_TITLE = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { + defaultMessage: 'Case Title', +}); + +export const CASE_ID = i18n.translate('xpack.siem.caseTable.columnHeader.caseId', { + defaultMessage: 'Case Id', +}); + +export const TAGS = i18n.translate('xpack.siem.caseTable.columnHeader.tags', { + defaultMessage: 'Tags', +}); + +export const CREATED_AT = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { + defaultMessage: 'Created at', +}); + +export const CREATED_BY = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { + defaultMessage: 'Created by', +}); + +export const LAST_UPDATED = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { + defaultMessage: 'Last updated', +}); + +export const STATE = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { + defaultMessage: 'State', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/index.tsx deleted file mode 100644 index d1c2f7f64063e4..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as i18n from './translations'; -import { Criteria, ItemsPerRow, PaginatedTable } from '../../../../components/paginated_table'; - -import { getCasesColumns } from './columns'; -import { Direction, SortFieldCase } from '../../../../containers/case/types'; -import { useGetCases } from '../../../../containers/case/use_get_cases'; -import { CasesSearchBar } from '../../search_bar'; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; -export const CasesTable = React.memo(() => { - const [ - { - data, - isLoading, - isError, - table: { page, perPage, sortOrder, sortField }, - }, - doFetch, - ] = useGetCases(); - - const updateActivePage = (newPage: number) => - doFetch({ - page: newPage + 1, - }); - - const updateLimitPagination = (newLimit: number) => - doFetch({ - page: 1, // reset to first page - perPage: newLimit, - }); - - const onChange = (criteria: Criteria) => { - if (criteria.sort != null && criteria.sort.direction !== sortOrder) { - let newSort; - switch (criteria.sort.field) { - case 'attributes.state': - newSort = SortFieldCase.state; - break; - case 'attributes.created_at': - newSort = SortFieldCase.createdAt; - break; - case 'updated_at': - newSort = SortFieldCase.updatedAt; - break; - default: - newSort = SortFieldCase.createdAt; - } - doFetch({ - sortField: newSort, - sortOrder: criteria.sort.direction as Direction, - }); - } - }; - - const sorting = { field: `attributes.${sortField}`, direction: sortOrder }; - return isError ? null : ( - <> - - - - - - - } - headerUnit={i18n.UNIT(data.total)} - hideInspect={true} - id={'getCasesTable'} - itemsPerRow={rowItems} - limit={perPage} - limitResetsActivePage={false} - loading={isLoading} - loadPage={newPage => newPage} - onChange={onChange} - pageOfItems={data.saved_objects} - showMorePagesIndicator={false} - sorting={sorting} - totalCount={data.total} - updateActivePage={updateActivePage} - updateLimitPagination={updateLimitPagination} - /> - - - - ); -}); - -CasesTable.displayName = 'CasesTable'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/translations.ts deleted file mode 100644 index 7d5c8bc1b8f7a6..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/cases_table/translations.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const ALL_CASES = i18n.translate('xpack.siem.caseTable.title', { - defaultMessage: 'All Cases', -}); - -export const ROWS_5 = i18n.translate('xpack.siem.caseTable.rowsFive', { - values: { numRows: 5 }, - defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', -}); - -export const ROWS_10 = i18n.translate('xpack.siem.caseTable.rowsTen', { - values: { numRows: 10 }, - defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', -}); - -export const UNIT = (totalCount: number) => - i18n.translate('xpack.siem.caseTable.unit', { - values: { totalCount }, - defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, - }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx index 3f80efc3d72da8..eb2b940fd5529a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { Route, Switch, RouteComponentProps } from 'react-router-dom'; import { SiemPageName } from '../home/types'; -import { CasePage } from './case'; -import { CaseDetails } from './case_details'; +import { CasesPage } from './case'; +import { CaseDetailsPage } from './case_details'; type Props = Partial> & { url: string }; @@ -19,7 +19,7 @@ const caseDetailsPagePath = `${casesPagePath}/:detailName`; const CaseContainerComponent: React.FC = () => { return ( - } /> + } /> = () => { match: { params: { detailName }, }, - }) => } + }) => } /> ); }; export const Case = React.memo(CaseContainerComponent); -Case.displayName = 'Case'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index c6c45479e44476..480518e436800d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -38,4 +38,3 @@ export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.case.emptyActionP export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.case.emptyActionSecondary', { defaultMessage: 'Go to documentation', }); - From 90926e3fc9ef8c236b1aebd210b56216715dbb4d Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 23 Jan 2020 15:44:02 -0700 Subject: [PATCH 19/67] pretty up table --- .../case/components/all_cases/columns.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 13598b4ffbc848..92a27c33f08ca2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiBadge, EuiTableFieldDataColumnType } from '@elastic/eui'; +import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType } from '@elastic/eui'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { CaseSavedObject } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -12,22 +12,20 @@ import { CaseDetailsLink } from '../../../../components/links'; import { TruncatableText } from '../../../../components/truncatable_text'; import * as i18n from './translations'; -export type CasesColumns = EuiTableFieldDataColumnType; +export type CasesColumns = + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType; const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); export const getCasesColumns = (): CasesColumns[] => [ { - field: 'attributes.title', name: i18n.CASE_TITLE, - render: title => renderStringField(title), - }, - { - field: 'id', - name: i18n.CASE_ID, - render: id => { - if (id != null) { - return ; + render: (theCase: CaseSavedObject) => { + if (theCase.id != null && theCase.attributes!.title) { + return ( + {theCase.attributes.title} + ); } return getEmptyTagValue(); }, @@ -35,11 +33,11 @@ export const getCasesColumns = (): CasesColumns[] => [ { field: 'attributes.tags', name: i18n.TAGS, - render: tags => { + render: (tags: CaseSavedObject['attributes']['tags']) => { if (tags != null && tags.length > 0) { return ( - {tags.map((tag, i) => ( + {tags.map((tag: string, i: number) => ( {tag} @@ -55,7 +53,7 @@ export const getCasesColumns = (): CasesColumns[] => [ field: 'attributes.created_at', name: i18n.CREATED_AT, sortable: true, - render: createdAt => { + render: (createdAt: CaseSavedObject['attributes']['created_at']) => { if (createdAt != null) { return ; } @@ -65,13 +63,14 @@ export const getCasesColumns = (): CasesColumns[] => [ { field: 'attributes.created_by.username', name: i18n.CREATED_BY, - render: createdBy => renderStringField(createdBy), + render: (createdBy: CaseSavedObject['attributes']['created_by']['username']) => + renderStringField(createdBy), }, { field: 'updated_at', name: i18n.LAST_UPDATED, sortable: true, - render: updatedAt => { + render: (updatedAt: CaseSavedObject['updated_at']) => { if (updatedAt != null) { return ; } @@ -82,6 +81,6 @@ export const getCasesColumns = (): CasesColumns[] => [ field: 'attributes.state', name: i18n.STATE, sortable: true, - render: state => renderStringField(state), + render: (state: CaseSavedObject['attributes']['state']) => renderStringField(state), }, ]; From a9fdcc62e3f1f1ccae727b0fecbeb1804b2991df Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 24 Jan 2020 12:59:40 -0700 Subject: [PATCH 20/67] ready for hw --- .../siem/public/containers/case/constants.ts | 2 +- .../public/containers/case/translations.ts | 18 + .../siem/public/containers/case/types.ts | 20 + .../public/containers/case/use_get_case.tsx | 49 +-- .../public/containers/case/use_get_cases.tsx | 48 ++- .../public/containers/case/use_get_tags.tsx | 92 +++++ .../siem/public/containers/case/utils.ts | 17 + .../plugins/siem/public/pages/case/case.tsx | 2 + .../case/components/all_cases/columns.tsx | 32 +- .../pages/case/components/all_cases/index.tsx | 28 +- .../components/all_cases/table_filters.tsx | 67 ++++ .../case/components/all_cases/translations.ts | 34 +- .../pages/case/components/case_view/index.tsx | 16 +- .../case/components/search_bar/index.tsx | 5 +- .../siem/public/pages/case/translations.ts | 20 +- .../server/lib/case/saved_object_mappings.ts | 13 +- .../lib/detection_engine/rules/find_rules.ts | 2 +- .../api/__fixtures__/mock_saved_objects.ts | 6 +- .../case/server/routes/api/get_tags.ts | 28 ++ .../plugins/case/server/routes/api/index.ts | 10 +- .../plugins/case/server/routes/api/schema.ts | 2 - .../plugins/case/server/routes/api/types.ts | 7 +- .../case/server/routes/api/update_case.ts | 5 +- .../plugins/case/server/routes/api/utils.ts | 1 + x-pack/plugins/case/server/services/index.ts | 11 + .../server/services/tags/read_tags.test.ts | 364 ++++++++++++++++++ .../case/server/services/tags/read_tags.ts | 86 +++++ 27 files changed, 849 insertions(+), 136 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/utils.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx create mode 100644 x-pack/plugins/case/server/routes/api/get_tags.ts create mode 100644 x-pack/plugins/case/server/services/tags/read_tags.test.ts create mode 100644 x-pack/plugins/case/server/services/tags/read_tags.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index 5dc45a82c6ef61..e9cee35bc5152d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -7,6 +7,6 @@ export const FETCH_INIT = 'FETCH_INIT'; export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const FETCH_FAILURE = 'FETCH_FAILURE'; -export const UPDATE_TABLE = 'UPDATE_TABLE'; +export const UPDATE_PAGINATION = 'UPDATE_PAGINATION'; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts new file mode 100644 index 00000000000000..0c8b896e2b426b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -0,0 +1,18 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle', { + defaultMessage: 'Error fetching data', +}); + +export const TAG_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.case.tagFetchFailDescription', + { + defaultMessage: 'Failed to fetch Tags', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 2f35f1f0478fa7..bdd7c7669fee18 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -29,6 +29,26 @@ export interface CaseResult { state: string; tags: string[] | []; title: string; + updated_at: number; +} + +export interface FilterOptions { + filter: string; + sortField: string; + sortOrder: 'asc' | 'desc'; + tags?: string[]; +} + +export interface FlattenedCaseSavedObject extends CaseResult { + id: string; + type: string; + version: string; +} +export interface FlattenedCasesSavedObjects { + saved_objects: FlattenedCaseSavedObject[] | []; + page: number; + per_page: number; + total: number; } export enum SortFieldCase { createdAt = 'created_at', diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index fa9f14c9726c7c..16b8268117bcc9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -7,17 +7,22 @@ import { useEffect, useReducer, useState } from 'react'; import chrome from 'ui/chrome'; -import { CaseSavedObject } from './types'; +import { FlattenedCaseSavedObject } from './types'; import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; +import { flattenSavedObject } from './utils'; +import { throwIfNotOk } from '../../hooks/api/api'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { useStateToaster } from '../../components/toasters'; interface CaseState { - data: CaseSavedObject; + data: FlattenedCaseSavedObject; isLoading: boolean; isError: boolean; } interface Action { type: string; - payload?: CaseSavedObject; + payload?: FlattenedCaseSavedObject; } const dataFetchReducer = (state: CaseState, action: Action): CaseState => { @@ -29,12 +34,12 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { isError: false, }; case FETCH_SUCCESS: - const getSavedObject = (a: Action['payload']) => a as CaseSavedObject; + const getTypedPayload = (a: Action['payload']) => a as FlattenedCaseSavedObject; return { ...state, isLoading: false, isError: false, - data: getSavedObject(action.payload), + data: getTypedPayload(action.payload), }; case FETCH_FAILURE: return { @@ -46,21 +51,19 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { throw new Error(); } }; -const initialData: CaseSavedObject = { - attributes: { - case_type: '', - created_at: 0, - created_by: { - username: '', - }, - description: '', - state: '', - tags: [], - title: '', +const initialData: FlattenedCaseSavedObject = { + case_type: '', + created_at: 0, + created_by: { + username: '', }, + description: '', + state: '', + tags: [], + title: '', id: '', type: '', - updated_at: '', + updated_at: 0, version: '', }; export const useGetCase = (initialCaseId: string): [CaseState] => { @@ -70,13 +73,14 @@ export const useGetCase = (initialCaseId: string): [CaseState] => { data: initialData, }); const [caseId, setCaseId] = useState(initialCaseId); + const [, dispatchToaster] = useStateToaster(); useEffect(() => { let didCancel = false; const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const result = await fetch( + const response = await fetch( `${chrome.getBasePath()}/api/cases/${caseId}?includeComments=false`, { method: 'GET', @@ -88,14 +92,13 @@ export const useGetCase = (initialCaseId: string): [CaseState] => { } ); if (!didCancel) { - const resultJson = await result.json(); - if (resultJson.statusCode >= 400) { - return dispatch({ type: FETCH_FAILURE }); - } - dispatch({ type: FETCH_SUCCESS, payload: resultJson }); + await throwIfNotOk(response); + const responseJson = await response.json(); + dispatch({ type: FETCH_SUCCESS, payload: flattenSavedObject(responseJson) }); } } catch (error) { if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); dispatch({ type: FETCH_FAILURE }); } } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index f5f1f2f61834ea..925d4016e33502 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -5,18 +5,22 @@ */ import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; - import chrome from 'ui/chrome'; + +import { throwIfNotOk } from '../../hooks/api/api'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT, FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, - UPDATE_TABLE, + UPDATE_PAGINATION, } from './constants'; -import { CasesSavedObjects, Direction, SortFieldCase } from './types'; - +import { FlattenedCasesSavedObjects, Direction, SortFieldCase } from './types'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { flattenSavedObjects } from './utils'; interface PaginationArgs { page: number; perPage: number; @@ -32,17 +36,14 @@ interface QueryArgs { } interface CasesState { - data: CasesSavedObjects; + data: FlattenedCasesSavedObjects; isLoading: boolean; isError: boolean; pagination: PaginationArgs; } -interface PayloadObj { - [key: string]: unknown; -} interface Action { type: string; - payload?: CasesSavedObjects | QueryArgs | PayloadObj; + payload?: FlattenedCasesSavedObjects | QueryArgs; } const dataFetchReducer = (state: CasesState, action: Action): CasesState => { @@ -54,12 +55,12 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { isError: false, }; case FETCH_SUCCESS: - const getSavedObject = (a: Action['payload']) => a as CasesSavedObjects; + const getTypedPayload = (a: Action['payload']) => a as FlattenedCasesSavedObjects; return { ...state, isLoading: false, isError: false, - data: getSavedObject(action.payload), + data: getTypedPayload(action.payload), }; case FETCH_FAILURE: return { @@ -67,7 +68,7 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { isLoading: false, isError: true, }; - case UPDATE_TABLE: + case UPDATE_PAGINATION: return { ...state, pagination: { @@ -79,7 +80,7 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { throw new Error(); } }; -const initialData: CasesSavedObjects = { +const initialData: FlattenedCasesSavedObjects = { page: 0, per_page: 0, total: 0, @@ -98,9 +99,10 @@ export const useGetCases = (): [CasesState, Dispatch>] }, }); const [query, setQuery] = useState(state.pagination as QueryArgs); + const [, dispatchToaster] = useStateToaster(); useEffect(() => { - dispatch({ type: UPDATE_TABLE, payload: query }); + dispatch({ type: UPDATE_PAGINATION, payload: query }); }, [query]); useEffect(() => { @@ -112,7 +114,7 @@ export const useGetCases = (): [CasesState, Dispatch>] (acc, [key, value]) => `${acc}${key}=${value}&`, '?' ); - const result = await fetch(`${chrome.getBasePath()}/api/cases${queryParams}`, { + const response = await fetch(`${chrome.getBasePath()}/api/cases${queryParams}`, { method: 'GET', credentials: 'same-origin', headers: { @@ -122,15 +124,19 @@ export const useGetCases = (): [CasesState, Dispatch>] }, }); if (!didCancel) { - const resultJson = await result.json(); - if (resultJson.statusCode >= 400) { - dispatch({ type: FETCH_FAILURE }); - } else { - dispatch({ type: FETCH_SUCCESS, payload: resultJson }); - } + await throwIfNotOk(response); + const responseJson = await response.json(); + dispatch({ + type: FETCH_SUCCESS, + payload: { + ...responseJson, + saved_objects: flattenSavedObjects(responseJson.saved_objects), + }, + }); } } catch (error) { if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); dispatch({ type: FETCH_FAILURE }); } } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx new file mode 100644 index 00000000000000..f796ae550c9ec3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -0,0 +1,92 @@ +/* + * 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 { useEffect, useReducer } from 'react'; +import chrome from 'ui/chrome'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; +import { throwIfNotOk } from '../../hooks/api/api'; + +interface TagsState { + data: string[]; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: string[]; +} + +const dataFetchReducer = (state: TagsState, action: Action): TagsState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + const getTypedPayload = (a: Action['payload']) => a as string[]; + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: string[] = []; + +export const useGetTags = (): [TagsState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await fetch(`${chrome.getBasePath()}/api/cases/tags`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + }, + }); + if (!didCancel) { + await throwIfNotOk(response); + const responseJson = await response.json(); + dispatch({ type: FETCH_SUCCESS, payload: responseJson }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, []); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts new file mode 100644 index 00000000000000..935e74c7263029 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts @@ -0,0 +1,17 @@ +/* + * 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 { CaseSavedObject, FlattenedCaseSavedObject } from './types'; + +export const flattenSavedObjects = (savedObjects: CaseSavedObject[]): FlattenedCaseSavedObject[] => + savedObjects.reduce((acc: FlattenedCaseSavedObject[], savedObject: CaseSavedObject) => { + return [...acc, flattenSavedObject(savedObject)]; + }, []); + +export const flattenSavedObject = (savedObject: CaseSavedObject): FlattenedCaseSavedObject => ({ + ...savedObject, + ...savedObject.attributes, +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 0ca9356dfa482a..934c68d884db8a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; +import { CasesSearchBar } from './components/search_bar'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; @@ -24,6 +25,7 @@ export const CasesPage = React.memo(() => ( subtitle={i18n.PAGE_SUBTITLE} title={i18n.PAGE_TITLE} /> + {/* */} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 92a27c33f08ca2..d625ec18590dcd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -6,34 +6,32 @@ import React from 'react'; import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType } from '@elastic/eui'; import { getEmptyTagValue } from '../../../../components/empty_value'; -import { CaseSavedObject } from '../../../../containers/case/types'; +import { FlattenedCaseSavedObject } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; import { CaseDetailsLink } from '../../../../components/links'; import { TruncatableText } from '../../../../components/truncatable_text'; import * as i18n from './translations'; export type CasesColumns = - | EuiTableFieldDataColumnType - | EuiTableComputedColumnType; + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType; const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); export const getCasesColumns = (): CasesColumns[] => [ { name: i18n.CASE_TITLE, - render: (theCase: CaseSavedObject) => { - if (theCase.id != null && theCase.attributes!.title) { - return ( - {theCase.attributes.title} - ); + render: (theCase: FlattenedCaseSavedObject) => { + if (theCase.id != null && theCase.title != null) { + return {theCase.title}; } return getEmptyTagValue(); }, }, { - field: 'attributes.tags', + field: 'tags', name: i18n.TAGS, - render: (tags: CaseSavedObject['attributes']['tags']) => { + render: (tags: FlattenedCaseSavedObject['tags']) => { if (tags != null && tags.length > 0) { return ( @@ -50,10 +48,10 @@ export const getCasesColumns = (): CasesColumns[] => [ truncateText: true, }, { - field: 'attributes.created_at', + field: 'created_at', name: i18n.CREATED_AT, sortable: true, - render: (createdAt: CaseSavedObject['attributes']['created_at']) => { + render: (createdAt: FlattenedCaseSavedObject['created_at']) => { if (createdAt != null) { return ; } @@ -61,16 +59,16 @@ export const getCasesColumns = (): CasesColumns[] => [ }, }, { - field: 'attributes.created_by.username', + field: 'created_by.username', name: i18n.CREATED_BY, - render: (createdBy: CaseSavedObject['attributes']['created_by']['username']) => + render: (createdBy: FlattenedCaseSavedObject['created_by']['username']) => renderStringField(createdBy), }, { field: 'updated_at', name: i18n.LAST_UPDATED, sortable: true, - render: (updatedAt: CaseSavedObject['updated_at']) => { + render: (updatedAt: FlattenedCaseSavedObject['updated_at']) => { if (updatedAt != null) { return ; } @@ -78,9 +76,9 @@ export const getCasesColumns = (): CasesColumns[] => [ }, }, { - field: 'attributes.state', + field: 'state', name: i18n.STATE, sortable: true, - render: (state: CaseSavedObject['attributes']['state']) => renderStringField(state), + render: (state: FlattenedCaseSavedObject['state']) => renderStringField(state), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 70e47c446fa2d5..70a9e93bde2d05 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -16,11 +16,17 @@ import { isEmpty } from 'lodash/fp'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; -import { Direction, SortFieldCase, CaseSavedObject } from '../../../../containers/case/types'; +import { + Direction, + SortFieldCase, + FlattenedCaseSavedObject, +} from '../../../../containers/case/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; import { HeaderSection } from '../../../../components/header_section'; +import { CasesTableFilters } from './table_filters'; + import { UtilityBar, UtilityBarGroup, @@ -37,11 +43,11 @@ export const AllCases = React.memo(() => { if (sort) { let newSort; switch (sort.field) { - case 'attributes.state': + case 'state': newSort = SortFieldCase.state; break; - case 'attributes.created_at': - newSort = 'attributes.created_at'; + case 'created_at': + newSort = SortFieldCase.createdAt; break; case 'updated_at': newSort = SortFieldCase.updatedAt; @@ -49,15 +55,10 @@ export const AllCases = React.memo(() => { default: newSort = SortFieldCase.createdAt; } - const oppositeSort = sort.direction === Direction.asc ? Direction.desc : Direction.asc; - const newOrder = - sort.direction === pagination.sortOrder && pagination.sortField === newSort - ? oppositeSort - : sort.direction; newPagination = { ...newPagination, sortField: newSort, - sortOrder: newOrder as Direction, + sortOrder: sort.direction as Direction, }; } if (page) { @@ -74,11 +75,12 @@ export const AllCases = React.memo(() => { const sorting = { sort: { field: pagination.sortField, direction: pagination.sortOrder }, - } as EuiTableSortingType; - return isError ? null : ( + } as EuiTableSortingType; + + return ( -

{`TableFilters placeholder`}

+ {}} />
{isLoading && isEmpty(data.saved_objects) && ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx new file mode 100644 index 00000000000000..6343ad4f1beb64 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -0,0 +1,67 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; + +import { EuiFieldSearch, EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +import { FilterOptions } from '../../../../containers/case/types'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; +import { TagsFilterPopover } from '../../../../pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover'; + +interface CasesTableFiltersProps { + onFilterChanged: (filterOptions: Partial) => void; +} + +/** + * Collection of filters for filtering data within the CasesTable. Contains search bar, + * and tag selection + * + * @param onFilterChanged change listener to be notified on filter changes + */ +const CasesTableFiltersComponent = ({ onFilterChanged }: CasesTableFiltersProps) => { + const [filter, setFilter] = useState(''); + const [selectedTags, setSelectedTags] = useState([]); + const [{ isLoading, data }] = useGetTags(); + + // Propagate filter changes to parent + useEffect(() => { + onFilterChanged({ filter, tags: selectedTags }); + }, [filter, selectedTags, onFilterChanged]); + + const handleOnSearch = useCallback(filterString => setFilter(filterString.trim()), [setFilter]); + + return ( + + + + + + + + + + + + ); +}; + +CasesTableFiltersComponent.displayName = 'CasesTableFiltersComponent'; + +export const CasesTableFilters = React.memo(CasesTableFiltersComponent); + +CasesTableFilters.displayName = 'CasesTableFilters'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index de8a4bde7bbf14..7f13d9e83a8ac5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -20,41 +20,51 @@ export const ADD_NEW_CASE = i18n.translate('xpack.siem.caseTable.addNewCase', { }); export const SHOWING_CASES = (totalRules: number) => - i18n.translate('xpack.siem.caseTable.showingCasesTitle', { + i18n.translate('xpack.siem.case.caseTable.showingCasesTitle', { values: { totalRules }, defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {case} other {cases}}', }); export const UNIT = (totalCount: number) => - i18n.translate('xpack.siem.caseTable.unit', { + i18n.translate('xpack.siem.case.caseTable.unit', { values: { totalCount }, defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, }); -export const CASE_TITLE = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { +export const CASE_TITLE = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { defaultMessage: 'Case Title', }); -export const CASE_ID = i18n.translate('xpack.siem.caseTable.columnHeader.caseId', { - defaultMessage: 'Case Id', -}); - -export const TAGS = i18n.translate('xpack.siem.caseTable.columnHeader.tags', { +export const TAGS = i18n.translate('xpack.siem.case.caseTable.columnHeader.tags', { defaultMessage: 'Tags', }); -export const CREATED_AT = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { +export const CREATED_AT = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { defaultMessage: 'Created at', }); -export const CREATED_BY = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { +export const CREATED_BY = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { defaultMessage: 'Created by', }); -export const LAST_UPDATED = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { +export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { defaultMessage: 'Last updated', }); -export const STATE = i18n.translate('xpack.siem.caseTable.columnHeader.caseTitle', { +export const STATE = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { defaultMessage: 'State', }); + +export const SEARCH_CASES = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchAriaLabel', + { + defaultMessage: 'Search cases', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchPlaceholder', + { + defaultMessage: 'e.g. case name', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 14241fbed7f7b0..d525e9e71fd49e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -37,15 +37,15 @@ export const CaseView = React.memo(({ caseId }: Props) => { const caseDetailsDefinitions = [ { title: , - definition: data.attributes.description, + definition: data.description, }, { title: , - definition: data.attributes.case_type, + definition: data.case_type, }, { title: , - definition: data.attributes.state, + definition: data.state, }, { title: , @@ -53,18 +53,18 @@ export const CaseView = React.memo(({ caseId }: Props) => { }, { title: , - definition: data.attributes.created_at, + definition: data.created_at, }, { title: , - definition: data.attributes.created_by.username, + definition: data.created_by.username, }, { title: , definition: - data.attributes.tags.length > 0 ? ( + data.tags.length > 0 ? (
    - {data.attributes.tags.map((tag: string, key: number) => ( + {data.tags.map((tag: string, key: number) => (
  • {tag}
  • ))}
@@ -82,7 +82,7 @@ export const CaseView = React.memo(({ caseId }: Props) => { {i18n.BACK_LABEL} - +
{caseDetailsDefinitions.map((dictionaryItem, key) => diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx index 55b493e33412d9..ec1a46848fd0f2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* tslint:disable */ +/* eslint-disable */ import React, { useState } from 'react'; import { EuiHealth, @@ -276,7 +278,6 @@ export const CasesSearchBar = React.memo(() => { const queriedItems = EuiSearchBar.Query.execute(currentQuery, items, { defaultFields: ['owner', 'tag', 'type'], }); - console.log('queriedItems', queriedItems); return ; }; @@ -306,3 +307,5 @@ export const CasesSearchBar = React.memo(() => { }); CasesSearchBar.displayName = 'CasesSearchBar'; +/* eslint-enable */ +/* tslint:enable */ diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 480518e436800d..4b7c02726dab2b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -10,31 +10,19 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { defaultMessage: 'Case Workflows', }); -export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.pageSubtitle', { +export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.casePage.pageSubtitle', { defaultMessage: 'Case Workflow Management within the Elastic SIEM', }); -export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.pageBadgeLabel', { +export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.casePage.pageBadgeLabel', { defaultMessage: 'Beta', }); -export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.pageBadgeTooltip', { +export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.casePage.pageBadgeTooltip', { defaultMessage: 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', }); -export const BACK_LABEL = i18n.translate('xpack.siem.case.pageBackLabel', { +export const BACK_LABEL = i18n.translate('xpack.siem.case.casePage.pageBackLabel', { defaultMessage: '< Back to all cases', }); - -export const EMPTY_TITLE = i18n.translate('xpack.siem.case.emptyTitle', { - defaultMessage: 'It looks like you don’t have any indices relevant to the SIEM application', -}); - -export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.case.emptyActionPrimary', { - defaultMessage: 'View setup instructions', -}); - -export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.case.emptyActionSecondary', { - defaultMessage: 'Go to documentation', -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts index bd73805600a338..4853ffad557c74 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts @@ -23,16 +23,6 @@ export const caseSavedObjectMappings: { } = { [caseSavedObjectType]: { properties: { - assignees: { - properties: { - username: { - type: 'keyword', - }, - full_name: { - type: 'keyword', - }, - }, - }, created_at: { type: 'date', }, @@ -61,6 +51,9 @@ export const caseSavedObjectMappings: { case_type: { type: 'keyword', }, + updated_at: { + type: 'date', + }, }, }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts index e193e123f42817..cdeddcb484cf66 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts @@ -16,7 +16,7 @@ export const getFilter = (filter: string | null | undefined) => { } }; -export const findRules = async ({ +export const cli = async ({ alertsClient, perPage, page, diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index d59f0977e69934..e35f920b59b6de 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -19,7 +19,7 @@ export const mockCases = [ state: 'open', tags: ['defacement'], case_type: 'security', - assignees: [], + updated_at: 1574718888885, }, references: [], updated_at: '2019-11-25T21:54:48.952Z', @@ -39,7 +39,7 @@ export const mockCases = [ state: 'open', tags: ['Data Destruction'], case_type: 'security', - assignees: [], + updated_at: 1574721130834, }, references: [], updated_at: '2019-11-25T22:32:00.900Z', @@ -59,7 +59,7 @@ export const mockCases = [ state: 'open', tags: ['LOLBins'], case_type: 'security', - assignees: [], + updated_at: 1574721147881, }, references: [], updated_at: '2019-11-25T22:32:17.947Z', diff --git a/x-pack/plugins/case/server/routes/api/get_tags.ts b/x-pack/plugins/case/server/routes/api/get_tags.ts new file mode 100644 index 00000000000000..1d714db4c0c288 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/get_tags.ts @@ -0,0 +1,28 @@ +/* + * 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 { RouteDeps } from './index'; +import { wrapError } from './utils'; + +export function initGetTagsApi({ caseService, router }: RouteDeps) { + router.get( + { + path: '/api/cases/tags', + validate: {}, + }, + async (context, request, response) => { + let theCase; + try { + theCase = await caseService.getTags({ + client: context.core.savedObjects.client, + }); + return response.ok({ body: theCase }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index 11ef91d539e87b..32dfd6a78d1c28 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -5,17 +5,18 @@ */ import { IRouter } from 'src/core/server'; -import { initDeleteCommentApi } from './delete_comment'; +import { CaseServiceSetup } from '../../services'; import { initDeleteCaseApi } from './delete_case'; +import { initDeleteCommentApi } from './delete_comment'; import { initGetAllCaseCommentsApi } from './get_all_case_comments'; import { initGetAllCasesApi } from './get_all_cases'; import { initGetCaseApi } from './get_case'; import { initGetCommentApi } from './get_comment'; +import { initGetTagsApi } from './get_tags'; import { initPostCaseApi } from './post_case'; import { initPostCommentApi } from './post_comment'; import { initUpdateCaseApi } from './update_case'; import { initUpdateCommentApi } from './update_comment'; -import { CaseServiceSetup } from '../../services'; export interface RouteDeps { caseService: CaseServiceSetup; @@ -23,12 +24,13 @@ export interface RouteDeps { } export function initCaseApi(deps: RouteDeps) { + initDeleteCaseApi(deps); + initDeleteCommentApi(deps); initGetAllCaseCommentsApi(deps); initGetAllCasesApi(deps); initGetCaseApi(deps); initGetCommentApi(deps); - initDeleteCaseApi(deps); - initDeleteCommentApi(deps); + initGetTagsApi(deps); initPostCaseApi(deps); initPostCommentApi(deps); initUpdateCaseApi(deps); diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 30222c163c8624..f9295e300c2cb2 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -26,7 +26,6 @@ export const UpdatedCommentSchema = schema.object({ }); export const NewCaseSchema = schema.object({ - assignees: schema.arrayOf(UserSchema, { defaultValue: [] }), description: schema.string(), title: schema.string(), state: schema.oneOf([schema.literal('open'), schema.literal('closed')], { defaultValue: 'open' }), @@ -35,7 +34,6 @@ export const NewCaseSchema = schema.object({ }); export const UpdatedCaseSchema = schema.object({ - assignees: schema.maybe(schema.arrayOf(UserSchema)), description: schema.maybe(schema.string()), title: schema.maybe(schema.string()), state: schema.maybe(schema.oneOf([schema.literal('open'), schema.literal('closed')])), diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index 7fe06e9ea1058a..ab200549646a49 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -26,13 +26,14 @@ export type UserType = TypeOf; export interface NewCaseFormatted extends NewCaseType { created_at: number; created_by: UserType; + updated_at: number; } export interface UpdatedCaseType { - assignees?: UpdatedCaseTyped['assignees']; + case_type?: UpdatedCaseTyped['case_type']; description?: UpdatedCaseTyped['description']; - title?: UpdatedCaseTyped['title']; state?: UpdatedCaseTyped['state']; tags?: UpdatedCaseTyped['tags']; - case_type?: UpdatedCaseTyped['case_type']; + title?: UpdatedCaseTyped['title']; + updated_at: number; } diff --git a/x-pack/plugins/case/server/routes/api/update_case.ts b/x-pack/plugins/case/server/routes/api/update_case.ts index 52c8cab0022dd1..cf1e100300c980 100644 --- a/x-pack/plugins/case/server/routes/api/update_case.ts +++ b/x-pack/plugins/case/server/routes/api/update_case.ts @@ -25,7 +25,10 @@ export function initUpdateCaseApi({ caseService, router }: RouteDeps) { const updatedCase = await caseService.updateCase({ client: context.core.savedObjects.client, caseId: request.params.id, - updatedAttributes: request.body, + updatedAttributes: { + ...request.body, + updated_at: new Date().valueOf(), + }, }); return response.ok({ body: updatedCase }); } catch (error) { diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index c6e33dbb8433bd..16b81c06510a80 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -20,6 +20,7 @@ export const formatNewCase = ( ): NewCaseFormatted => ({ created_at: new Date().valueOf(), created_by: { full_name, username }, + updated_at: new Date().valueOf(), ...newCase, }); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index a3f218670afe2d..c9a64a252bb6d5 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -26,6 +26,7 @@ import { AuthenticatedUser, PluginSetupContract as SecurityPluginSetup, } from '../../../security/server'; +import { readTags } from './tags/read_tags'; interface ClientArgs { client: SavedObjectsClientContract; @@ -73,6 +74,7 @@ export interface CaseServiceSetup { getAllCaseComments(args: GetCaseArgs): Promise; getCase(args: GetCaseArgs): Promise; getComment(args: GetCommentArgs): Promise; + getTags(args: ClientArgs): Promise; getUser(args: GetUserArgs): Promise; postNewCase(args: PostCaseArgs): Promise; postNewComment(args: PostCommentArgs): Promise; @@ -140,6 +142,15 @@ export class CaseService { throw error; } }, + getTags: async ({ client }: ClientArgs) => { + try { + this.log.debug(`Attempting to GET all cases`); + return await readTags({ client }); + } catch (error) { + this.log.debug(`Error on GET cases: ${error}`); + throw error; + } + }, getUser: async ({ request, response }: GetUserArgs) => { let user; try { diff --git a/x-pack/plugins/case/server/services/tags/read_tags.test.ts b/x-pack/plugins/case/server/services/tags/read_tags.test.ts new file mode 100644 index 00000000000000..87739bf785012e --- /dev/null +++ b/x-pack/plugins/case/server/services/tags/read_tags.test.ts @@ -0,0 +1,364 @@ +/* + * 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 { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { AlertsClient } from '../../../../../alerting'; +import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; +import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; +import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; + +describe('read_tags', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('readRawTags', () => { + test('it should return the intersection of tags to where none are repeating', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should return the intersection of tags to where some are repeating values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should work with no tags defined between two results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should work with a single tag which has repeating values in it', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2']); + }); + + test('it should work with a single tag which has empty tags', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + }); + + describe('readTags', () => { + test('it should return the intersection of tags to where none are repeating', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should return the intersection of tags to where some are repeating values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should work with no tags defined between two results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should work with a single tag which has repeating values in it', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2']); + }); + + test('it should work with a single tag which has empty tags', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should filter out any __internal tags for things such as alert_id', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + ]; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1']); + }); + + test('it should filter out any __internal tags with two different results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + 'tag 2', + 'tag 3', + 'tag 4', + 'tag 5', + ]; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + 'tag 2', + 'tag 3', + 'tag 4', + ]; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); + }); + }); + + describe('convertTagsToSet', () => { + test('it should convert the intersection of two tag systems without duplicates', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits({ data: [result1, result2] }); + const set = convertTagsToSet(findResult.data); + expect(Array.from(set)).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should with with an empty array', () => { + const set = convertTagsToSet([]); + expect(Array.from(set)).toEqual([]); + }); + }); + + describe('convertToTags', () => { + test('it should convert the two tag systems together with duplicates', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits({ data: [result1, result2] }); + const tags = convertToTags(findResult.data); + expect(tags).toEqual([ + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 4', + ]); + }); + + test('it should filter out anything that is not a tag', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '99979e67-19a7-455f-b452-8eded6135716'; + result2.params.ruleId = 'rule-2'; + delete result2.tags; + + const result3 = getResult(); + result3.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result3.params.ruleId = 'rule-2'; + result3.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits({ data: [result1, result2, result3] }); + const tags = convertToTags(findResult.data); + expect(tags).toEqual([ + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 4', + ]); + }); + + test('it should with with an empty array', () => { + const tags = convertToTags([]); + expect(tags).toEqual([]); + }); + }); + + describe('isTags', () => { + test('it should return true if the object has a tags on it', () => { + expect(isTags({ tags: [] })).toBe(true); + }); + + test('it should return false if the object does not have a tags on it', () => { + expect(isTags({})).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/case/server/services/tags/read_tags.ts b/x-pack/plugins/case/server/services/tags/read_tags.ts new file mode 100644 index 00000000000000..832b8951558a64 --- /dev/null +++ b/x-pack/plugins/case/server/services/tags/read_tags.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { has } from 'lodash/fp'; +import { SavedObjectsClientContract } from 'kibana/server'; +import { CASE_SAVED_OBJECT } from '../../constants'; + +const DEFAULT_PER_PAGE: number = 1000; +interface TagAttr { + tags: string[]; +} +export interface TagType { + id: string; + attributes: TagAttr; +} + +export const isTags = (obj: object): obj is TagType => { + return has('attributes.tags', obj); +}; + +export const convertToTags = (tagObjects: object[]): string[] => { + const tags = tagObjects.reduce((accum, tagObj) => { + if (isTags(tagObj)) { + return [...accum, ...tagObj.attributes.tags]; + } else { + return accum; + } + }, []); + return tags; +}; + +export const convertTagsToSet = (tagObjects: object[]): Set => { + return new Set(convertToTags(tagObjects)); +}; + +// Note: This is doing an in-memory aggregation of the tags by calling each of the alerting +// records in batches of this const setting and uses the fields to try to get the least +// amount of data per record back. If saved objects at some point supports aggregations +// then this should be replaced with a an aggregation call. +// Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html +export const readTags = async ({ + client, + perPage = DEFAULT_PER_PAGE, +}: { + client: SavedObjectsClientContract; + perPage?: number; +}): Promise => { + const tags = await readRawTags({ client, perPage }); + return tags; +}; + +export const readRawTags = async ({ + client, + perPage = DEFAULT_PER_PAGE, +}: { + client: SavedObjectsClientContract; + perPage?: number; +}): Promise => { + const firstTags = await client.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags'], + page: 1, + perPage, + }); + const firstSet = convertTagsToSet(firstTags.saved_objects); + const totalPages = Math.ceil(firstTags.total / firstTags.per_page); + if (totalPages <= 1) { + return Array.from(firstSet); + } else { + const returnTags = await Array(totalPages - 1) + .fill({}) + .map((_, page) => { + // page index starts at 2 as we already got the first page and we have more pages to go + return client.find({ type: CASE_SAVED_OBJECT, fields: ['tags'], perPage, page: page + 2 }); + }) + .reduce>>(async (accum, nextTagPage) => { + const tagArray = convertToTags((await nextTagPage).saved_objects); + return new Set([...(await accum), ...tagArray]); + }, Promise.resolve(firstSet)); + + return Array.from(returnTags); + } +}; From 3a3ceb923a9892bbc6cb2898b3935f40db333295 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 28 Jan 2020 15:25:30 -0500 Subject: [PATCH 21/67] [SIEM][CASE] Add package.json to fix lodash dependencies issues --- x-pack/plugins/case/package.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 x-pack/plugins/case/package.json diff --git a/x-pack/plugins/case/package.json b/x-pack/plugins/case/package.json new file mode 100644 index 00000000000000..aa63cfd564bcae --- /dev/null +++ b/x-pack/plugins/case/package.json @@ -0,0 +1,14 @@ +{ + "author": "Elastic", + "name": "cases", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": {}, + "dependencies": { + "@types/lodash": "^4.14.110" + }, + "devDependencies": { + "lodash": "^4.17.15" + } +} From 636e3c2c39d310f95788b3307c70a9f5cbb255de Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 13:31:29 -0500 Subject: [PATCH 22/67] [SIEM][CASE] Add case base url to constants --- x-pack/legacy/plugins/siem/public/containers/case/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index e9cee35bc5152d..362322c1560cba 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -10,3 +10,5 @@ export const FETCH_FAILURE = 'FETCH_FAILURE'; export const UPDATE_PAGINATION = 'UPDATE_PAGINATION'; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; + +export const CASES_URL = `/api/cases`; From 992765d48ee36c0705e0ca5d03143aa26c95dba2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 13:34:44 -0500 Subject: [PATCH 23/67] [SIEM][CASE] Create PaginationOptions interface --- x-pack/legacy/plugins/siem/public/containers/case/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index bdd7c7669fee18..3a81ee40f4a93d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -79,3 +79,10 @@ export interface ElasticUser { username: string; full_name?: string; } + +export interface PaginationOptions { + page: number; + perPage: number; + total: number; +} + From 48bf037ab8cbdaf16d97e990446b3b83fbdfaf86 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 13:35:33 -0500 Subject: [PATCH 24/67] [SIEM][CASE] Create Case interface --- .../plugins/siem/public/containers/case/types.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 3a81ee40f4a93d..dc478cb15b210e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -7,6 +7,17 @@ import { SavedObjectsBaseOptions } from 'kibana/server'; export { Direction } from '../../graphql/types'; +interface Case { + case_type: string; + created_at: number; + created_by: ElasticUser; + description: string; + state: string; + tags: string[] | []; + title: string; + updated_at: number; +} + export interface CasesSavedObjects { saved_objects: CaseSavedObject[] | []; page: number; From ab2df758b57485eb60aa817d4b1223064a36c4cc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 13:36:07 -0500 Subject: [PATCH 25/67] [SIEM][CASE] Create FetchCaseProps interface --- x-pack/legacy/plugins/siem/public/containers/case/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index dc478cb15b210e..6e8a4b10d3e201 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -97,3 +97,8 @@ export interface PaginationOptions { total: number; } +export interface FetchCasesProps { + pagination?: PaginationOptions; + filterOptions?: FilterOptions; +} + From 2d422bf688a8c2671103c8ffc26a0dc9cac0066f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 13:36:26 -0500 Subject: [PATCH 26/67] [SIEM][CASE] Create FetchCasesResponse interface --- x-pack/legacy/plugins/siem/public/containers/case/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 6e8a4b10d3e201..43e931a219d0be 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -102,3 +102,9 @@ export interface FetchCasesProps { filterOptions?: FilterOptions; } +export interface FetchCasesResponse { + page: number; + perPage: number; + total: number; + data: Case[]; +} From 4ba52a96d9856d5087237af57388a81998250161 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 15:45:21 -0500 Subject: [PATCH 27/67] [SIEM][CASE] Add saved_object to cases response --- x-pack/legacy/plugins/siem/public/containers/case/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 43e931a219d0be..ea11aa96d17d28 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -106,5 +106,6 @@ export interface FetchCasesResponse { page: number; perPage: number; total: number; + saved_objects: CaseSavedObject[]; data: Case[]; } From 07f3aa73e4a7b7abf17b4b678c8b7efdff02d2d4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 15:47:17 -0500 Subject: [PATCH 28/67] [SIEM][CASE] Move types --- .../siem/public/containers/case/types.ts | 34 +++++++++++++++---- .../public/containers/case/use_get_cases.tsx | 27 ++------------- .../pages/case/components/all_cases/index.tsx | 4 ++- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index ea11aa96d17d28..c093ccc6f6cf4c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsBaseOptions } from 'kibana/server'; -export { Direction } from '../../graphql/types'; +import { Direction } from '../../graphql/types'; interface Case { case_type: string; @@ -43,6 +43,32 @@ export interface CaseResult { updated_at: number; } +export interface PaginationOptions { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: Direction; +} + +export interface QueryArgs { + page?: number; + perPage?: number; + sortField?: SortFieldCase; + sortOrder?: Direction; +} + +export interface CasesState { + data: FlattenedCasesSavedObjects; + isLoading: boolean; + isError: boolean; + pagination: PaginationOptions; + filterOptions: FilterOptions; +} +export interface Action { + type: string; + payload?: FlattenedCasesSavedObjects | QueryArgs; +} + export interface FilterOptions { filter: string; sortField: string; @@ -91,12 +117,6 @@ export interface ElasticUser { full_name?: string; } -export interface PaginationOptions { - page: number; - perPage: number; - total: number; -} - export interface FetchCasesProps { pagination?: PaginationOptions; filterOptions?: FilterOptions; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 925d4016e33502..1e2bc9f25189f7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -16,35 +16,12 @@ import { FETCH_SUCCESS, UPDATE_PAGINATION, } from './constants'; -import { FlattenedCasesSavedObjects, Direction, SortFieldCase } from './types'; +import { FlattenedCasesSavedObjects, SortFieldCase, CasesState, QueryArgs, Action } from './types'; +import { Direction } from '../../graphql/types'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { flattenSavedObjects } from './utils'; -interface PaginationArgs { - page: number; - perPage: number; - sortField: SortFieldCase; - sortOrder: Direction; -} - -interface QueryArgs { - page?: number; - perPage?: number; - sortField?: SortFieldCase; - sortOrder?: Direction; -} - -interface CasesState { - data: FlattenedCasesSavedObjects; - isLoading: boolean; - isError: boolean; - pagination: PaginationArgs; -} -interface Action { - type: string; - payload?: FlattenedCasesSavedObjects | QueryArgs; -} const dataFetchReducer = (state: CasesState, action: Action): CasesState => { switch (action.type) { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 70a9e93bde2d05..428477696dedc5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -17,10 +17,12 @@ import * as i18n from './translations'; import { getCasesColumns } from './columns'; import { - Direction, SortFieldCase, FlattenedCaseSavedObject, + FilterOptions, } from '../../../../containers/case/types'; + +import { Direction } from '../../../../graphql/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; From 7530ef9886f23555a84199bd3723593a7df37398 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 16:06:49 -0500 Subject: [PATCH 29/67] [SIEM][CASE] Create api --- .../siem/public/containers/case/api.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/api.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts new file mode 100644 index 00000000000000..510962fd3c5f1e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -0,0 +1,42 @@ +/* + * 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 chrome from 'ui/chrome'; +import { FetchCasesProps, FetchCasesResponse, SortFieldCase } from './types'; +import { Direction } from '../../graphql/types'; +import { throwIfNotOk } from '../../hooks/api/api'; +import { CASES_URL } from './constants'; + +export const fetchCases = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + sortField: SortFieldCase.createdAt, + sortOrder: Direction.desc, + }, +}: FetchCasesProps): Promise => { + const queryParams = Object.entries(pagination).reduce( + (acc, [key, value]) => `${acc}${key}=${value}&`, + '?' + ); + const response = await fetch(`${chrome.getBasePath()}${CASES_URL}${queryParams}`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', + }, + }); + await throwIfNotOk(response); + return response.json(); +}; From 3a5c37780156d7c4f0e3be639d96f044e8460b95 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jan 2020 16:07:20 -0500 Subject: [PATCH 30/67] [SIEM][CASE] Use api --- .../public/containers/case/use_get_cases.tsx | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 1e2bc9f25189f7..cf767e81cc26d7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -5,9 +5,7 @@ */ import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; -import chrome from 'ui/chrome'; -import { throwIfNotOk } from '../../hooks/api/api'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT, @@ -22,6 +20,7 @@ import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { flattenSavedObjects } from './utils'; +import { fetchCases } from './api'; const dataFetchReducer = (state: CasesState, action: Action): CasesState => { switch (action.type) { @@ -68,6 +67,11 @@ export const useGetCases = (): [CasesState, Dispatch>] isLoading: false, isError: false, data: initialData, + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + }, pagination: { page: DEFAULT_TABLE_ACTIVE_PAGE, perPage: DEFAULT_TABLE_LIMIT, @@ -87,27 +91,16 @@ export const useGetCases = (): [CasesState, Dispatch>] const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const queryParams = Object.entries(state.pagination).reduce( - (acc, [key, value]) => `${acc}${key}=${value}&`, - '?' - ); - const response = await fetch(`${chrome.getBasePath()}/api/cases${queryParams}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + const response = await fetchCases({ + filterOptions: state.filterOptions, + pagination: state.pagination, }); if (!didCancel) { - await throwIfNotOk(response); - const responseJson = await response.json(); dispatch({ type: FETCH_SUCCESS, payload: { - ...responseJson, - saved_objects: flattenSavedObjects(responseJson.saved_objects), + ...response, + saved_objects: flattenSavedObjects(response.saved_objects), }, }); } From 987fdd2deddc4a43bfd09a7b9a5ba3eb715abdbd Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jan 2020 10:32:14 -0500 Subject: [PATCH 31/67] [SIEM][CASE] Add capability to API to filter by tags --- .../legacy/plugins/siem/public/containers/case/api.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 510962fd3c5f1e..9e7da76f37e369 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -24,10 +24,19 @@ export const fetchCases = async ({ sortOrder: Direction.desc, }, }: FetchCasesProps): Promise => { - const queryParams = Object.entries(pagination).reduce( + let queryParams = Object.entries(pagination).reduce( (acc, [key, value]) => `${acc}${key}=${value}&`, '?' ); + + const filters = [ + ...(filterOptions.tags?.map(t => `case-workflow.attributes.tags:${encodeURIComponent(t)}`) ?? + []), + ]; + + const filterParams = `filter=${filters.join('%20AND%20')}`; + queryParams = `${queryParams}${filterParams}`; + const response = await fetch(`${chrome.getBasePath()}${CASES_URL}${queryParams}`, { method: 'GET', credentials: 'same-origin', From c8c3f99c30c05480810abd89652e237b25a83754 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jan 2020 10:33:51 -0500 Subject: [PATCH 32/67] [SIEM][CASE] Add constant --- x-pack/legacy/plugins/siem/public/containers/case/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index 362322c1560cba..46daa2f3eb25d1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -8,6 +8,7 @@ export const FETCH_INIT = 'FETCH_INIT'; export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const FETCH_FAILURE = 'FETCH_FAILURE'; export const UPDATE_PAGINATION = 'UPDATE_PAGINATION'; +export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; From 68f1a17152029ffcfbc846cd45677ccc112ea4f4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jan 2020 10:34:15 -0500 Subject: [PATCH 33/67] [SIEM][CASE] Add FilterOptions to payload --- x-pack/legacy/plugins/siem/public/containers/case/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index c093ccc6f6cf4c..f54b6ef9f0b77a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -66,7 +66,7 @@ export interface CasesState { } export interface Action { type: string; - payload?: FlattenedCasesSavedObjects | QueryArgs; + payload?: FlattenedCasesSavedObjects | QueryArgs | FilterOptions; } export interface FilterOptions { From 24e251590bbb0c7b226593525b8f5efba25e377f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jan 2020 10:35:18 -0500 Subject: [PATCH 34/67] [SIEM][CASE] Update filter options on reducer --- .../siem/public/containers/case/use_get_cases.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index cf767e81cc26d7..9df6a40be935b0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -13,8 +13,16 @@ import { FETCH_INIT, FETCH_SUCCESS, UPDATE_PAGINATION, + UPDATE_FILTER_OPTIONS, } from './constants'; -import { FlattenedCasesSavedObjects, SortFieldCase, CasesState, QueryArgs, Action } from './types'; +import { + FlattenedCasesSavedObjects, + SortFieldCase, + CasesState, + QueryArgs, + Action, + FilterOptions, +} from './types'; import { Direction } from '../../graphql/types'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; @@ -52,6 +60,11 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { ...action.payload, }, }; + case UPDATE_FILTER_OPTIONS: + return { + ...state, + filterOptions: action.payload, + }; default: throw new Error(); } From fb4cddfc43f5f40c0168b5cde87b7baa0fcf066d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jan 2020 10:36:38 -0500 Subject: [PATCH 35/67] [SIEM][CASE] Dispatch filter changes --- .../siem/public/containers/case/use_get_cases.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 9df6a40be935b0..6d29e2075f36dd 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -75,7 +75,11 @@ const initialData: FlattenedCasesSavedObjects = { total: 0, saved_objects: [], }; -export const useGetCases = (): [CasesState, Dispatch>] => { +export const useGetCases = (): [ + CasesState, + Dispatch>, + Dispatch> +] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, @@ -93,12 +97,17 @@ export const useGetCases = (): [CasesState, Dispatch>] }, }); const [query, setQuery] = useState(state.pagination as QueryArgs); + const [filterQuery, setFilters] = useState(state.filterOptions as FilterOptions); const [, dispatchToaster] = useStateToaster(); useEffect(() => { dispatch({ type: UPDATE_PAGINATION, payload: query }); }, [query]); + useEffect(() => { + dispatch({ type: UPDATE_FILTER_OPTIONS, payload: filterQuery }); + }, [filterQuery]); + useEffect(() => { let didCancel = false; const fetchData = async () => { @@ -128,6 +137,6 @@ export const useGetCases = (): [CasesState, Dispatch>] return () => { didCancel = true; }; - }, [state.pagination]); - return [state, setQuery]; + }, [state.pagination, state.filterOptions]); + return [state, setQuery, setFilters]; }; From 705a313d2f280bfaf9fe6ff972fd529c91ce54a8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jan 2020 10:37:46 -0500 Subject: [PATCH 36/67] [SIEM][CASE] Filter options from UI table --- .../pages/case/components/all_cases/index.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 428477696dedc5..2bb90b03af933f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -37,7 +37,11 @@ import { } from '../../../../components/detection_engine/utility_bar'; export const AllCases = React.memo(() => { - const [{ data, isLoading, isError, pagination }, doFetch] = useGetCases(); + const [ + { data, isLoading, isError, pagination, filterOptions }, + doFetch, + setFilters, + ] = useGetCases(); const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { @@ -75,14 +79,18 @@ export const AllCases = React.memo(() => { [doFetch, pagination] ); - const sorting = { + const onFilterChangedCallback = useCallback((newFilterOptions: Partial) => { + setFilters({ ...filterOptions, ...newFilterOptions }); + }, []); + + const sorting: EuiTableSortingType = { sort: { field: pagination.sortField, direction: pagination.sortOrder }, - } as EuiTableSortingType; + }; return ( - {}} /> + {isLoading && isEmpty(data.saved_objects) && ( From 420ff6836e816e95374d127a0db2fc0cc5a6f5ef Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 3 Feb 2020 14:37:06 -0700 Subject: [PATCH 37/67] set up search field --- .../siem/public/containers/case/api.ts | 15 +++++------ .../siem/public/containers/case/types.ts | 4 +-- .../public/containers/case/use_get_cases.tsx | 10 +++---- .../components/all_cases/table_filters.tsx | 8 +++--- .../case/server/routes/api/get_all_cases.ts | 8 +++--- .../plugins/case/server/routes/api/schema.ts | 14 +++++++++- .../plugins/case/server/routes/api/types.ts | 8 ++++-- .../plugins/case/server/routes/api/utils.ts | 26 +++++++++++++++++++ x-pack/plugins/case/server/services/index.ts | 4 +-- 9 files changed, 68 insertions(+), 29 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 9e7da76f37e369..9df934b7b0d97b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -12,9 +12,7 @@ import { CASES_URL } from './constants'; export const fetchCases = async ({ filterOptions = { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', + search: '', tags: [], }, pagination = { @@ -28,15 +26,16 @@ export const fetchCases = async ({ (acc, [key, value]) => `${acc}${key}=${value}&`, '?' ); - - const filters = [ + const tags = [ ...(filterOptions.tags?.map(t => `case-workflow.attributes.tags:${encodeURIComponent(t)}`) ?? []), ]; - const filterParams = `filter=${filters.join('%20AND%20')}`; - queryParams = `${queryParams}${filterParams}`; - + const tagParams = `filter=${tags.join('%20AND%20')}&`; + const searchParams = `search=${ + filterOptions.search // filterOptions.search.length > 0 ? `*${filterOptions.search}*` : filterOptions.search + }&`; + queryParams = `${queryParams}${tagParams}${searchParams}`; const response = await fetch(`${chrome.getBasePath()}${CASES_URL}${queryParams}`, { method: 'GET', credentials: 'same-origin', diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index f54b6ef9f0b77a..0973430981970c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -70,9 +70,7 @@ export interface Action { } export interface FilterOptions { - filter: string; - sortField: string; - sortOrder: 'asc' | 'desc'; + search: string; tags?: string[]; } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 6d29e2075f36dd..8fb7c34ed72e30 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -31,6 +31,7 @@ import { flattenSavedObjects } from './utils'; import { fetchCases } from './api'; const dataFetchReducer = (state: CasesState, action: Action): CasesState => { + let getTypedPayload; switch (action.type) { case FETCH_INIT: return { @@ -39,7 +40,7 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { isError: false, }; case FETCH_SUCCESS: - const getTypedPayload = (a: Action['payload']) => a as FlattenedCasesSavedObjects; + getTypedPayload = (a: Action['payload']) => a as FlattenedCasesSavedObjects; return { ...state, isLoading: false, @@ -61,9 +62,10 @@ const dataFetchReducer = (state: CasesState, action: Action): CasesState => { }, }; case UPDATE_FILTER_OPTIONS: + getTypedPayload = (a: Action['payload']) => a as FilterOptions; return { ...state, - filterOptions: action.payload, + filterOptions: getTypedPayload(action.payload), }; default: throw new Error(); @@ -85,9 +87,7 @@ export const useGetCases = (): [ isError: false, data: initialData, filterOptions: { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', + search: '', }, pagination: { page: DEFAULT_TABLE_ACTIVE_PAGE, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index 6343ad4f1beb64..a706d55582c165 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -24,16 +24,16 @@ interface CasesTableFiltersProps { * @param onFilterChanged change listener to be notified on filter changes */ const CasesTableFiltersComponent = ({ onFilterChanged }: CasesTableFiltersProps) => { - const [filter, setFilter] = useState(''); + const [search, setSearch] = useState(''); const [selectedTags, setSelectedTags] = useState([]); const [{ isLoading, data }] = useGetTags(); // Propagate filter changes to parent useEffect(() => { - onFilterChanged({ filter, tags: selectedTags }); - }, [filter, selectedTags, onFilterChanged]); + onFilterChanged({ search, tags: selectedTags }); + }, [search, selectedTags, onFilterChanged]); - const handleOnSearch = useCallback(filterString => setFilter(filterString.trim()), [setFilter]); + const handleOnSearch = useCallback(searchString => setSearch(searchString.trim()), [setSearch]); return ( diff --git a/x-pack/plugins/case/server/routes/api/get_all_cases.ts b/x-pack/plugins/case/server/routes/api/get_all_cases.ts index 09c81d63dc791b..87b0de8a54bf93 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_cases.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_cases.ts @@ -6,15 +6,15 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; -import { SavedOptionsFindOptionsSchema } from './schema'; +import { formatSavedOptionsFind, wrapError } from './utils'; +import { SavedObjectsFindOptionsSchema } from './schema'; export function initGetAllCasesApi({ caseService, router }: RouteDeps) { router.get( { path: '/api/cases', validate: { - query: schema.nullable(SavedOptionsFindOptionsSchema), + query: schema.nullable(SavedObjectsFindOptionsSchema), }, }, async (context, request, response) => { @@ -22,7 +22,7 @@ export function initGetAllCasesApi({ caseService, router }: RouteDeps) { const args = request.query ? { client: context.core.savedObjects.client, - options: request.query, + options: formatSavedOptionsFind(request.query), } : { client: context.core.savedObjects.client, diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index f9295e300c2cb2..51008f2cbc61bc 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -41,7 +41,19 @@ export const UpdatedCaseSchema = schema.object({ case_type: schema.maybe(schema.string()), }); -export const SavedOptionsFindOptionsSchema = schema.object({ +export const SavedObjectsFindOptionsSchema = schema.object({ + defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), + fields: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + search: schema.maybe(schema.string()), + searchFields: schema.maybe(schema.string()), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), +}); + +export const SavedObjectsFindOptionsSchemaFormatted = schema.object({ defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), fields: schema.maybe(schema.arrayOf(schema.string())), filter: schema.maybe(schema.string()), diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index ab200549646a49..dd5e0b9bda5fdf 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -9,7 +9,8 @@ import { CommentSchema, NewCaseSchema, NewCommentSchema, - SavedOptionsFindOptionsSchema, + SavedObjectsFindOptionsSchema, + SavedObjectsFindOptionsSchemaFormatted, UpdatedCaseSchema, UpdatedCommentSchema, UserSchema, @@ -18,7 +19,10 @@ import { export type NewCaseType = TypeOf; export type NewCommentFormatted = TypeOf; export type NewCommentType = TypeOf; -export type SavedOptionsFindOptionsType = TypeOf; +export type SavedObjectsFindOptionsType = TypeOf; +export type SavedObjectsFindOptionsTypeFormatted = TypeOf< + typeof SavedObjectsFindOptionsSchemaFormatted +>; export type UpdatedCaseTyped = TypeOf; export type UpdatedCommentType = TypeOf; export type UserType = TypeOf; diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 16b81c06510a80..3fdfd3fb0ab262 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -11,6 +11,8 @@ import { NewCaseFormatted, NewCommentType, NewCommentFormatted, + SavedObjectsFindOptionsType, + SavedObjectsFindOptionsTypeFormatted, UserType, } from './types'; @@ -47,3 +49,27 @@ export function wrapError(error: any): CustomHttpResponseOptions statusCode: boom.output.statusCode, }; } + +export const formatSavedOptionsFind = ( + savedObjectsFindOptions: SavedObjectsFindOptionsType +): SavedObjectsFindOptionsTypeFormatted => { + let options = { ...savedObjectsFindOptions }; + if (savedObjectsFindOptions.fields && savedObjectsFindOptions.fields.length > 0) { + options = { + ...options, + fields: JSON.parse(savedObjectsFindOptions.fields), + }; + } else { + delete options.fields; + } + if (savedObjectsFindOptions.searchFields && savedObjectsFindOptions.searchFields.length > 0) { + options = { + ...options, + searchFields: JSON.parse(savedObjectsFindOptions.searchFields), + }; + } else { + delete options.searchFields; + } + // wtf ts + return options; +}; diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index c9a64a252bb6d5..06f20355d88a7e 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -18,7 +18,7 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../constants'; import { NewCaseFormatted, NewCommentFormatted, - SavedOptionsFindOptionsType, + SavedObjectsFindOptionsTypeFormatted, UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; @@ -37,7 +37,7 @@ interface GetCaseArgs extends ClientArgs { } interface GetCasesArgs extends ClientArgs { - options?: SavedOptionsFindOptionsType; + options?: SavedObjectsFindOptionsTypeFormatted; } interface GetCommentArgs extends ClientArgs { commentId: string; From db80bffffc637b0d5fe1d171451c1ce7e5082bac Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 3 Feb 2020 16:36:28 -0700 Subject: [PATCH 38/67] add new page for create case --- .../siem/public/components/link_to/index.ts | 8 +++++- .../public/components/link_to/link_to.tsx | 7 +++++- .../components/link_to/redirect_to_case.tsx | 3 +++ .../siem/public/components/links/index.tsx | 13 +++++++++- .../navigation/breadcrumbs/index.ts | 5 ---- .../pages/case/components/create/index.tsx | 10 ++++++++ .../siem/public/pages/case/create_case.tsx | 25 +++++++++++++++++++ .../plugins/siem/public/pages/case/index.tsx | 5 +++- .../siem/public/pages/case/translations.ts | 4 +++ .../plugins/siem/public/pages/case/utils.ts | 12 +++++++-- 10 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index 85bf9a128652f8..c93b415e017bb7 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -13,4 +13,10 @@ export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines'; -export { getCaseUrl, getCaseDetailsUrl, RedirectToCasePage } from './redirect_to_case'; +export { + getCaseDetailsUrl, + getCaseUrl, + getCreateCaseUrl, + RedirectToCasePage, + RedirectToCreatePage, +} from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index b49c3149ae4c69..bc81fcd90345bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,7 +20,7 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; -import { RedirectToCasePage } from './redirect_to_case'; +import { RedirectToCasePage, RedirectToCreatePage } from './redirect_to_case'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { @@ -38,6 +38,11 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToCasePage} path={`${match.url}/:pageName(${SiemPageName.case})`} /> + ); +export const RedirectToCreatePage = () => ; + const baseCaseUrl = `#/link-to/${SiemPageName.case}`; export const getCaseUrl = () => baseCaseUrl; export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; +export const getCreateCaseUrl = () => `${baseCaseUrl}/create`; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index 66148b5bf8b05b..4f74f9ff2f5d60 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -8,7 +8,12 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { encodeIpv6 } from '../../lib/helpers'; -import { getCaseDetailsUrl, getHostDetailsUrl, getIPDetailsUrl } from '../link_to'; +import { + getCaseDetailsUrl, + getHostDetailsUrl, + getIPDetailsUrl, + getCreateCaseUrl, +} from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; // Internal Links @@ -46,6 +51,12 @@ const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailNam export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); CaseDetailsLink.displayName = 'CaseDetailsLink'; +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => ( + {children} +)); + +CreateCaseLink.displayName = 'CreateCaseLink'; + // External Links export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 6117bd4b1d9d8d..08f4a068078e0c 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -103,11 +103,6 @@ export const getBreadcrumbsForRoute = ( ]; } if (isCaseRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } return [...siemRootBreadcrumb, ...getCaseDetailsBreadcrumbs(spyState)]; } if ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx new file mode 100644 index 00000000000000..c460c24a54d8ee --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -0,0 +1,10 @@ +/* + * 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 from 'react'; + +export const Create = React.memo(() =>

{`Hello create case`}

); + +Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx new file mode 100644 index 00000000000000..0bb60f1af05a35 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -0,0 +1,25 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import React from 'react'; + +import { WrapperPage } from '../../components/wrapper_page'; +import { Create } from './components/create'; +import { SpyRoute } from '../../utils/route/spy_routes'; + +export const CreateCasePage = React.memo(() => ( + <> + + + + + + + +)); + +CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx index eb2b940fd5529a..6b38814e69ff29 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -8,18 +8,21 @@ import React from 'react'; import { Route, Switch, RouteComponentProps } from 'react-router-dom'; import { SiemPageName } from '../home/types'; -import { CasesPage } from './case'; import { CaseDetailsPage } from './case_details'; +import { CasesPage } from './case'; +import { CreateCasePage } from './create_case'; type Props = Partial> & { url: string }; const casesPagePath = `/:pageName(${SiemPageName.case})`; const caseDetailsPagePath = `${casesPagePath}/:detailName`; +const createCasePagePath = `${casesPagePath}/create`; const CaseContainerComponent: React.FC = () => { return ( } /> + } /> { href: getCaseUrl(), }, ]; - if (params.detailName != null) { + if (params.detailName === 'create') { + breadcrumb = [ + ...breadcrumb, + { + text: i18n.CREATE_TITLE, + href: getCreateCaseUrl(), + }, + ]; + } else if (params.detailName != null) { breadcrumb = [ ...breadcrumb, { From aaf9dc1aa7e77c0fdda3d72277e1889e7985a682 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 3 Feb 2020 17:00:47 -0700 Subject: [PATCH 39/67] wip, form panel --- .../plugins/siem/public/pages/case/case.tsx | 10 +++++++- .../pages/case/components/case_view/index.tsx | 15 ++++++++---- .../pages/case/components/create/index.tsx | 17 +++++++++++++- .../pages/case/components/shared_imports.ts | 23 +++++++++++++++++++ .../siem/public/pages/case/create_case.tsx | 23 +++++++++++++++---- .../siem/public/pages/case/translations.ts | 8 +++++++ .../plugins/siem/public/pages/case/utils.ts | 2 +- 7 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/shared_imports.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 934c68d884db8a..414aa5e4f131b0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -6,12 +6,14 @@ import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; import { CasesSearchBar } from './components/search_bar'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; +import { getCreateCaseUrl } from '../../components/link_to'; export const CasesPage = React.memo(() => ( <> @@ -24,7 +26,13 @@ export const CasesPage = React.memo(() => ( }} subtitle={i18n.PAGE_SUBTITLE} title={i18n.PAGE_TITLE} - /> + > + + + {i18n.CREATE_TITLE} + + + {/* */} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index d525e9e71fd49e..eece9e7187d3ea 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { HeaderPage } from '../../../../components/header_page'; @@ -79,10 +79,15 @@ export const CaseView = React.memo(({ caseId }: Props) => {
) : ( - - {i18n.BACK_LABEL} - - +
{caseDetailsDefinitions.map((dictionaryItem, key) => diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index c460c24a54d8ee..46f5d00a150a81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -4,7 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { EuiAccordion, EuiPanel } from '@elastic/eui'; +import styled, { StyledComponent } from 'styled-components'; -export const Create = React.memo(() =>

{`Hello create case`}

); +const CreateCaseAccordion: StyledComponent< + typeof EuiAccordion, + any, // eslint-disable-line + { ref: React.MutableRefObject }, + never +> = styled(EuiAccordion)` + .euiAccordion__childWrapper { + overflow: visible; + } +`; + +CreateCaseAccordion.displayName = 'CreateCaseAccordion'; + +export const Create = React.memo(() => {`helo`}); Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/shared_imports.ts new file mode 100644 index 00000000000000..4d1f1b3f6f3d3d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/shared_imports.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + FIELD_TYPES, + Form, + FormData, + FormDataProvider, + FormHook, + FormSchema, + UseField, + useForm, + ValidationFunc, +} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx index 0bb60f1af05a35..b7d6b57bee3e83 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -4,19 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; import { WrapperPage } from '../../components/wrapper_page'; import { Create } from './components/create'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { HeaderPage } from '../../components/header_page'; +import * as i18n from './translations'; +import { getCaseUrl } from '../../components/link_to'; export const CreateCasePage = React.memo(() => ( <> - - - - + + {' '} + + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 47f8404fe4fddc..64b6899f20f2d0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -11,9 +11,17 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { }); export const CREATE_TITLE = i18n.translate('xpack.siem.case.create.title', { + defaultMessage: 'Create new case', +}); + +export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.create.breadcrumb', { defaultMessage: 'Create', }); +export const BACK_TO_ALL = i18n.translate('xpack.siem.case.back.label', { + defaultMessage: 'Back to all cases', +}); + export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.casePage.pageSubtitle', { defaultMessage: 'Case Workflow Management within the Elastic SIEM', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts index 3da2448e71991d..bd6cb5da5eb01a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts @@ -20,7 +20,7 @@ export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => { breadcrumb = [ ...breadcrumb, { - text: i18n.CREATE_TITLE, + text: i18n.CREATE_BC_TITLE, href: getCreateCaseUrl(), }, ]; From 859f7ef4b644ea7c18009a53aea1d9a1766da2dc Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 4 Feb 2020 11:59:23 -0700 Subject: [PATCH 40/67] create new case page added --- .../siem/public/containers/case/api.ts | 23 ++- .../siem/public/containers/case/constants.ts | 1 + .../siem/public/containers/case/types.ts | 15 ++ .../public/containers/case/use_post_case.tsx | 100 +++++++++++++ .../pages/case/components/all_cases/index.tsx | 6 +- .../pages/case/components/create/index.tsx | 136 ++++++++++++++++-- .../create/optional_field_label/index.tsx | 16 +++ .../pages/case/components/create/schema.tsx | 71 +++++++++ .../siem/public/pages/case/create_case.tsx | 3 +- .../siem/public/pages/case/translations.ts | 8 +- .../plugins/case/server/routes/api/schema.ts | 10 +- 11 files changed, 360 insertions(+), 29 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 9df934b7b0d97b..1ad25d0dbe923f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -5,7 +5,13 @@ */ import chrome from 'ui/chrome'; -import { FetchCasesProps, FetchCasesResponse, SortFieldCase } from './types'; +import { + FetchCasesProps, + FetchCasesResponse, + NewCase, + NewCaseFormatted, + SortFieldCase, +} from './types'; import { Direction } from '../../graphql/types'; import { throwIfNotOk } from '../../hooks/api/api'; import { CASES_URL } from './constants'; @@ -48,3 +54,18 @@ export const fetchCases = async ({ await throwIfNotOk(response); return response.json(); }; + +export const createCase = async (newCase: NewCase): Promise => { + const response = await fetch(`${chrome.getBasePath()}${CASES_URL}`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', + }, + body: JSON.stringify(newCase), + }); + await throwIfNotOk(response); + return response.json(); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index 46daa2f3eb25d1..5c277a7b89bfad 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -9,6 +9,7 @@ export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const FETCH_FAILURE = 'FETCH_FAILURE'; export const UPDATE_PAGINATION = 'UPDATE_PAGINATION'; export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; +export const POST_NEW_CASE = 'POST_NEW_CASE'; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 0973430981970c..8fc6b7161c4ab2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -7,6 +7,21 @@ import { SavedObjectsBaseOptions } from 'kibana/server'; import { Direction } from '../../graphql/types'; +interface FormData { + isNew?: boolean; +} + +export interface NewCase extends FormData { + description: string; + tags: string[] | []; + title: string; + case_type: string; +} + +export interface NewCaseFormatted extends NewCase { + state: string; +} + interface Case { case_type: string; created_at: number; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx new file mode 100644 index 00000000000000..d51ddf9d90bc7f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -0,0 +1,100 @@ +/* + * 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 { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_CASE } from './constants'; +import { CaseSavedObject, NewCase } from './types'; +import { createCase } from './api'; + +interface NewCaseState { + data: NewCase; + newCase?: CaseSavedObject; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: NewCase | CaseSavedObject; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + let getTypedPayload; + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case POST_NEW_CASE: + getTypedPayload = (a: Action['payload']) => a as NewCase; + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_SUCCESS: + getTypedPayload = (a: Action['payload']) => a as CaseSavedObject; + return { + ...state, + isLoading: false, + isError: false, + newCase: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: NewCase = { + case_type: 'security', + description: '', + isNew: false, + tags: [], + title: '', +}; + +export const usePostCase = (): [NewCaseState, Dispatch>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [formData, setFormData] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + dispatch({ type: POST_NEW_CASE, payload: formData }); + }, [formData]); + + useEffect(() => { + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const dataWithoutIsNew = state.data; + delete dataWithoutIsNew.isNew; + const response = await createCase(dataWithoutIsNew); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.data.isNew) { + fetchData(); + } + }, [state.data.isNew]); + return [state, setFormData]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 2bb90b03af933f..620d9af2599dc2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -37,11 +37,7 @@ import { } from '../../../../components/detection_engine/utility_bar'; export const AllCases = React.memo(() => { - const [ - { data, isLoading, isError, pagination, filterOptions }, - doFetch, - setFilters, - ] = useGetCases(); + const [{ data, isLoading, pagination, filterOptions }, doFetch, setFilters] = useGetCases(); const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 46f5d00a150a81..c0fc27a4020381 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -3,23 +3,131 @@ * 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 from 'react'; -import { EuiAccordion, EuiPanel } from '@elastic/eui'; -import styled, { StyledComponent } from 'styled-components'; +import React, { useCallback } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { Redirect } from 'react-router-dom'; +import { Field, Form, getUseField, useForm } from '../shared_imports'; +import { NewCase } from '../../../../containers/case/types'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { schema } from './schema'; +import * as i18n from '../../translations'; +import { SiemPageName } from '../../../home/types'; -const CreateCaseAccordion: StyledComponent< - typeof EuiAccordion, - any, // eslint-disable-line - { ref: React.MutableRefObject }, - never -> = styled(EuiAccordion)` - .euiAccordion__childWrapper { - overflow: visible; - } +const CommonUseField = getUseField({ component: Field }); + +const TagContainer = styled.div` + margin-top: 16px; +`; +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; `; -CreateCaseAccordion.displayName = 'CreateCaseAccordion'; +const caseTypeOptions = [ + { + value: 'security', + inputDisplay: 'Security', + }, + { + value: 'other', + inputDisplay: 'Other', + }, +]; -export const Create = React.memo(() => {`helo`}); +export const Create = React.memo(() => { + const [{ data, isLoading, newCase }, setFormData] = usePostCase(); + const { form } = useForm({ + defaultValue: data, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid) { + setFormData({ ...newData, isNew: true } as NewCase); + } + }, [form]); + + if (newCase && newCase.id) { + return ; + } + return ( + + {isLoading && } +
+ + + + + + + + <> + + + + + {i18n.SUBMIT} + + + + +
+ ); +}); Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx new file mode 100644 index 00000000000000..b86198e09ceac4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx @@ -0,0 +1,16 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../../../translations'; + +export const OptionalFieldLabel = ( + + {i18n.OPTIONAL} + +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx new file mode 100644 index 00000000000000..1ace32f5bae310 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx @@ -0,0 +1,71 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, fieldValidators, FormSchema } from '../shared_imports'; +import { OptionalFieldLabel } from './optional_field_label'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.siem.case.createCase.fieldTitleLabel', { + defaultMessage: 'Title', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.siem.case.createCase.titleFieldRequiredError', { + defaultMessage: 'A title is required.', + }) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.siem.case.createCase.fieldDescriptionLabel', { + defaultMessage: 'Description', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.siem.case.createCase.descriptionFieldRequiredError', { + defaultMessage: 'A description is required.', + }) + ), + }, + ], + }, + case_type: { + type: FIELD_TYPES.SUPER_SELECT, + label: i18n.translate('xpack.siem.case.createCase.fieldSeverityLabel', { + defaultMessage: 'Case Type', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.siem.case.createCase.caseTypeFieldRequiredError', { + defaultMessage: 'A case type is required.', + }) + ), + }, + ], + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.siem.case.createCase.fieldTagsLabel', { + defaultMessage: 'Tags', + }), + helpText: i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', + }), + labelAppend: OptionalFieldLabel, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx index b7d6b57bee3e83..09fac84aeacc9a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -15,8 +15,7 @@ import { getCaseUrl } from '../../components/link_to'; export const CreateCasePage = React.memo(() => ( <> - - {' '} + Date: Tue, 4 Feb 2020 15:28:37 -0700 Subject: [PATCH 41/67] markdown support --- .../components/paginated_table/index.tsx | 2 - .../plugins/siem/public/pages/case/case.tsx | 4 +- .../pages/case/components/case_view/index.tsx | 8 +- .../pages/case/components/create/index.tsx | 15 +- .../pages/case/components/create/schema.tsx | 3 - .../description_md_editor/index.tsx | 96 +++++ .../pages/case/components/search_bar/data.ts | 96 ----- .../case/components/search_bar/index.tsx | 311 --------------- .../siem/public/pages/case/translations.ts | 10 +- .../plugins/case/server/routes/api/utils.ts | 21 +- .../server/services/tags/read_tags.test.ts | 364 ------------------ 11 files changed, 131 insertions(+), 799 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/data.ts delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx delete mode 100644 x-pack/plugins/case/server/services/tags/read_tags.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index e726803cdd5270..9890af40182cb9 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -22,7 +22,6 @@ import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; import styled from 'styled-components'; import { AuthTableColumns } from '../page/hosts/authentications_table'; -import { CasesColumns } from '../page/case/all_cases/columns'; import { HostsTableColumns } from '../page/hosts/hosts_table'; import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; @@ -73,7 +72,6 @@ declare type HostsTableColumnsTest = [ declare type BasicTableColumns = | AuthTableColumns - | CasesColumns | HostsTableColumns | HostsTableColumnsTest | NetworkDnsColumns diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 414aa5e4f131b0..f9abe5cb22da89 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -6,11 +6,10 @@ import React from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup } from '@elastic/eui'; import { HeaderPage } from '../../components/header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; -import { CasesSearchBar } from './components/search_bar'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; import { getCreateCaseUrl } from '../../components/link_to'; @@ -33,7 +32,6 @@ export const CasesPage = React.memo(() => ( - {/* */} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index eece9e7187d3ea..26d1f6052db08c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -8,10 +8,12 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { Markdown } from '../../../../components/markdown'; import { HeaderPage } from '../../../../components/header_page'; import * as i18n from '../../translations'; import { getCaseUrl } from '../../../../components/link_to'; import { useGetCase } from '../../../../containers/case/use_get_case'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; interface Props { caseId: string; @@ -37,7 +39,7 @@ export const CaseView = React.memo(({ caseId }: Props) => { const caseDetailsDefinitions = [ { title: , - definition: data.description, + definition: , }, { title: , @@ -49,11 +51,11 @@ export const CaseView = React.memo(({ caseId }: Props) => { }, { title: , - definition: data.updated_at, + definition: , }, { title: , - definition: data.created_at, + definition: , }, { title: , diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index c0fc27a4020381..456c9098d07887 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -20,8 +20,9 @@ import { usePostCase } from '../../../../containers/case/use_post_case'; import { schema } from './schema'; import * as i18n from '../../translations'; import { SiemPageName } from '../../../home/types'; +import { DescriptionMarkdown } from '../description_md_editor'; -const CommonUseField = getUseField({ component: Field }); +export const CommonUseField = getUseField({ component: Field }); const TagContainer = styled.div` margin-top: 16px; @@ -76,13 +77,11 @@ export const Create = React.memo(() => { isDisabled: isLoading, }} /> - setFormData({ ...data, description })} /> ` + height: ${({ height }) => height}px; + overflow: auto; +`; + +MarkdownContainer.displayName = 'MarkdownContainer'; + +const TextArea = styled(EuiTextArea)<{ height: number }>` + min-height: ${({ height }) => `${height}px`}; + width: 100%; +`; +TextArea.displayName = 'TextArea'; + +/** An input for entering a new case description */ +export const DescriptionMarkdown = React.memo<{ + descriptionInputHeight: number; + initialDescription: string; + isLoading: boolean; + onChange: (description: string) => void; +}>(({ initialDescription, isLoading, descriptionInputHeight, onChange }) => { + const [description, setDescription] = useState(initialDescription); + const tabs = [ + { + id: 'description', + name: i18n.DESCRIPTION, + content: ( + { + setDescription(e as string); + onChange(e as string); + }} + componentProps={{ + idAria: 'caseDescription', + 'data-test-subj': 'caseDescription', + isDisabled: isLoading, + spellcheck: false, + }} + /> + ), + }, + { + id: 'preview', + name: i18n.PREVIEW, + content: ( + + + + ), + }, + ]; + return ( + + + + 0} /> + + + ); +}); + +DescriptionMarkdown.displayName = 'DescriptionMarkdown'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/data.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/data.ts deleted file mode 100644 index 7636cecdb952d7..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/data.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const data = [ - { - type: 'case-workflow', - id: '058cbbd0-20da-11ea-b554-4dead8f3ebd9', - attributes: { - created_at: 1576593135435, - created_by: { full_name: null, username: 'elastic' }, - description: 'Urgent phishing case', - title: 'Suspicious email attachment opened', - state: 'open', - tags: ['phishing'], - case_type: 'security', - assignees: [ - { username: 'M', full_name: 'Classified' }, - { username: '007', full_name: 'James Bond' }, - ], - }, - references: [], - updated_at: '2019-12-17T14:32:15.629Z', - version: 'WzIxLDFd', - }, - { - type: 'case-workflow', - id: '335bf230-20d8-11ea-b554-4dead8f3ebd9', - attributes: { - created_at: 1576592353300, - created_by: { full_name: null, username: 'elastic' }, - title: 'Super Bad Security Issue', - description: 'bubblegum attack', - case_type: '', - assignees: [], - state: 'open', - tags: [], - }, - references: [], - updated_at: '2019-12-17T14:19:13.491Z', - version: 'WzIwLDFd', - }, - { - type: 'case-workflow', - id: '14583010-20d8-11ea-b554-4dead8f3ebd9', - attributes: { - created_at: 1576592301271, - created_by: { full_name: null, username: 'elastic' }, - title: 'Super Bad Security Issue', - description: 'bubblegum attack', - case_type: '', - assignees: [], - state: 'open', - tags: [], - }, - references: [], - updated_at: '2019-12-17T14:18:21.457Z', - version: 'WzE5LDFd', - }, - { - type: 'case-workflow', - id: 'bf9b5d80-20d3-11ea-b554-4dead8f3ebd9', - attributes: { - created_at: 1576590441102, - created_by: { full_name: null, username: 'elastic' }, - description: 'Urgent phishing case', - title: 'Suspicious email attachment opened', - state: 'open', - tags: ['phishing'], - case_type: 'security', - assignees: [{ username: 'Q' }], - }, - references: [], - updated_at: '2019-12-17T13:47:21.304Z', - version: 'WzE2LDFd', - }, - { - type: 'case-workflow', - id: '5cf3c4a0-20cf-11ea-b554-4dead8f3ebd9', - attributes: { - created_at: 1576588557506, - created_by: { full_name: null, username: 'elastic' }, - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - state: 'open', - tags: ['defacement'], - case_type: 'security', - assignees: [], - }, - references: [], - updated_at: '2019-12-17T13:15:57.802Z', - version: 'WzEzLDFd', - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx deleted file mode 100644 index ec1a46848fd0f2..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/search_bar/index.tsx +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* tslint:disable */ -/* eslint-disable */ -import React, { useState } from 'react'; -import { - EuiHealth, - EuiCallOut, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, - EuiBasicTable, - EuiSearchBar, - EuiButton, -} from '@elastic/eui'; - -const tags = [ - { name: 'marketing', color: 'danger' }, - { name: 'finance', color: 'success' }, - { name: 'eng', color: 'success' }, - { name: 'sales', color: 'warning' }, - { name: 'ga', color: 'success' }, -]; - -const types = ['dashboard', 'visualization', 'watch']; - -const users = ['dewey', 'wanda', 'carrie', 'jmack', 'gabic']; - -const items = [ - { - id: 100001, - status: 'open', - type: types[0], - tag: [tags[0].name, tags[1].name, tags[2].name], - active: true, - owner: users[0], - followers: 19, - comments: 7, - stars: 3, - }, - { - id: 100002, - status: 'open', - type: types[1], - tag: [tags[1].name, tags[4].name, tags[2].name], - active: true, - owner: users[2], - followers: 15, - comments: 6, - stars: 2, - }, - { - id: 100003, - status: 'closed', - type: types[0], - tag: [tags[2].name], - active: true, - owner: users[3], - followers: 11, - comments: 3, - stars: 1, - }, - { - id: 100004, - status: 'open', - type: types[2], - tag: [tags[2].name, tags[3].name], - active: false, - owner: users[2], - followers: 14, - comments: 8, - stars: 4, - }, -]; - -const loadTags = () => { - return new Promise(resolve => { - setTimeout(() => { - resolve( - tags.map(tag => ({ - value: tag.name, - view: {tag.name}, - })) - ); - }, 2000); - }); -}; - -const initialQuery = EuiSearchBar.Query.MATCH_ALL; - -export const CasesSearchBar = React.memo(() => { - const [currentQuery, setQuery] = useState(initialQuery); - const [currentError, setError] = useState(null); - const [incremental, setIncremental] = useState(false); - - const onChange = ({ query, error }) => { - if (error) { - setError(error); - } else { - setError(null); - setQuery(query); - } - }; - - const toggleIncremental = () => setIncremental(!incremental); - - const renderBookmarks = () => ( - <> -

{`Enter a query, or select one from a bookmark`}

- - - - setQuery('status:open owner:dewey')}> - {`mine, open`} - - - - setQuery('status:closed owner:dewey')}> - {`mine, closed`} - - - - - - ); - - const renderSearch = () => { - const filters = [ - { - type: 'field_value_toggle_group', - field: 'status', - items: [ - { - value: 'open', - name: 'Open', - }, - { - value: 'closed', - name: 'Closed', - }, - ], - }, - { - type: 'is', - field: 'active', - name: 'Active', - negatedName: 'Inactive', - }, - { - type: 'field_value_toggle', - name: 'Mine', - field: 'owner', - value: 'dewey', - }, - { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - multiSelect: 'or', - cache: 10000, // will cache the loaded tags for 10 sec - options: () => loadTags(), - }, - ]; - const schema = { - strict: true, - fields: { - active: { - type: 'boolean', - }, - status: { - type: 'string', - }, - followers: { - type: 'number', - }, - comments: { - type: 'number', - }, - stars: { - type: 'number', - }, - created: { - type: 'date', - }, - owner: { - type: 'string', - }, - tag: { - type: 'string', - validate: value => { - if (!tags.some(tag => tag.name === value)) { - throw new Error( - `unknown tag (possible values: ${tags.map(tag => tag.name).join(',')})` - ); - } - }, - }, - }, - }; - return ( - - ); - }; - - const renderError = () => { - if (!currentError) { - return; - } - return ( - <> - - - - ); - }; - - const renderTable = () => { - const columns = [ - { - name: 'Type', - field: 'type', - }, - { - name: 'Open', - field: 'status', - render: status => (status === 'open' ? 'Yes' : 'No'), - }, - { - name: 'Active', - field: 'active', - dataType: 'boolean', - }, - { - name: 'Tags', - field: 'tag', - render: itemTags => - itemTags.map((tag: string, key: number) => - key + 1 < itemTags.length ? `${tag}, ` : tag - ), - }, - { - name: 'Owner', - field: 'owner', - }, - { - name: 'Stats', - width: '150px', - render: item => { - return ( -
-
{`${item.stars} Stars`}
-
{`${item.followers} Followers`}
-
{`${item.comments} Comments`}
-
- ); - }, - }, - ]; - - const queriedItems = EuiSearchBar.Query.execute(currentQuery, items, { - defaultFields: ['owner', 'tag', 'type'], - }); - - return ; - }; - - const content = renderError() || ( - - {renderTable()} - - ); - - return ( - <> - - {renderBookmarks()} - - - {renderSearch()} - - - - - - - {content} - - ); -}); - -CasesSearchBar.displayName = 'CasesSearchBar'; -/* eslint-enable */ -/* tslint:enable */ diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 1bfae69580375e..f5e770d2837dcd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -39,6 +39,14 @@ export const OPTIONAL = i18n.translate('xpack.siem.case.casePage.optional', { defaultMessage: 'Optional', }); -export const SUBMIT = i18n.translate('xpack.siem.case.casePage.optional', { +export const SUBMIT = i18n.translate('xpack.siem.case.casePage.submit', { defaultMessage: 'Submit', }); + +export const DESCRIPTION = i18n.translate('xpack.siem.case.casePage.description', { + defaultMessage: 'Description', +}); + +export const PREVIEW = i18n.translate('xpack.siem.case.casePage.preview', { + defaultMessage: 'Preview', +}); diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 3fdfd3fb0ab262..64f5141ddd6c4d 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -53,23 +53,28 @@ export function wrapError(error: any): CustomHttpResponseOptions export const formatSavedOptionsFind = ( savedObjectsFindOptions: SavedObjectsFindOptionsType ): SavedObjectsFindOptionsTypeFormatted => { - let options = { ...savedObjectsFindOptions }; + let options: SavedObjectsFindOptionsTypeFormatted = { + defaultSearchOperator: savedObjectsFindOptions.defaultSearchOperator, + filter: savedObjectsFindOptions.filter, + page: savedObjectsFindOptions.page, + perPage: savedObjectsFindOptions.perPage, + search: savedObjectsFindOptions.search, + sortField: savedObjectsFindOptions.sortField, + sortOrder: savedObjectsFindOptions.sortOrder, + searchFields: undefined, + fields: undefined, + }; if (savedObjectsFindOptions.fields && savedObjectsFindOptions.fields.length > 0) { options = { ...options, - fields: JSON.parse(savedObjectsFindOptions.fields), + fields: JSON.parse(savedObjectsFindOptions.fields) as string[], }; - } else { - delete options.fields; } if (savedObjectsFindOptions.searchFields && savedObjectsFindOptions.searchFields.length > 0) { options = { ...options, - searchFields: JSON.parse(savedObjectsFindOptions.searchFields), + searchFields: JSON.parse(savedObjectsFindOptions.searchFields) as string[], }; - } else { - delete options.searchFields; } - // wtf ts return options; }; diff --git a/x-pack/plugins/case/server/services/tags/read_tags.test.ts b/x-pack/plugins/case/server/services/tags/read_tags.test.ts deleted file mode 100644 index 87739bf785012e..00000000000000 --- a/x-pack/plugins/case/server/services/tags/read_tags.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { AlertsClient } from '../../../../../alerting'; -import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; -import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; -import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; - -describe('read_tags', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('readRawTags', () => { - test('it should return the intersection of tags to where none are repeating', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 3']; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should return the intersection of tags to where some are repeating values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should work with no tags defined between two results', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = []; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual([]); - }); - - test('it should work with a single tag which has repeating values in it', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1', 'tag 2']); - }); - - test('it should work with a single tag which has empty tags', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual([]); - }); - }); - - describe('readTags', () => { - test('it should return the intersection of tags to where none are repeating', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 3']; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should return the intersection of tags to where some are repeating values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should work with no tags defined between two results', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = []; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual([]); - }); - - test('it should work with a single tag which has repeating values in it', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1', 'tag 2']); - }); - - test('it should work with a single tag which has empty tags', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual([]); - }); - - test('it should filter out any __internal tags for things such as alert_id', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = [ - `${INTERNAL_IDENTIFIER}_some_value`, - `${INTERNAL_RULE_ID_KEY}_some_value`, - 'tag 1', - ]; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1']); - }); - - test('it should filter out any __internal tags with two different results', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = [ - `${INTERNAL_IDENTIFIER}_some_value`, - `${INTERNAL_RULE_ID_KEY}_some_value`, - 'tag 1', - 'tag 2', - 'tag 3', - 'tag 4', - 'tag 5', - ]; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = [ - `${INTERNAL_IDENTIFIER}_some_value`, - `${INTERNAL_RULE_ID_KEY}_some_value`, - 'tag 1', - 'tag 2', - 'tag 3', - 'tag 4', - ]; - - const alertsClient = alertsClientMock.create(); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); - }); - }); - - describe('convertTagsToSet', () => { - test('it should convert the intersection of two tag systems without duplicates', () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const findResult = getFindResultWithMultiHits({ data: [result1, result2] }); - const set = convertTagsToSet(findResult.data); - expect(Array.from(set)).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should with with an empty array', () => { - const set = convertTagsToSet([]); - expect(Array.from(set)).toEqual([]); - }); - }); - - describe('convertToTags', () => { - test('it should convert the two tag systems together with duplicates', () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const findResult = getFindResultWithMultiHits({ data: [result1, result2] }); - const tags = convertToTags(findResult.data); - expect(tags).toEqual([ - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 4', - ]); - }); - - test('it should filter out anything that is not a tag', () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getResult(); - result2.id = '99979e67-19a7-455f-b452-8eded6135716'; - result2.params.ruleId = 'rule-2'; - delete result2.tags; - - const result3 = getResult(); - result3.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result3.params.ruleId = 'rule-2'; - result3.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const findResult = getFindResultWithMultiHits({ data: [result1, result2, result3] }); - const tags = convertToTags(findResult.data); - expect(tags).toEqual([ - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 4', - ]); - }); - - test('it should with with an empty array', () => { - const tags = convertToTags([]); - expect(tags).toEqual([]); - }); - }); - - describe('isTags', () => { - test('it should return true if the object has a tags on it', () => { - expect(isTags({ tags: [] })).toBe(true); - }); - - test('it should return false if the object does not have a tags on it', () => { - expect(isTags({})).toBe(false); - }); - }); -}); From 454505f647c64a7fa142491288f2c01ee0296885 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 4 Feb 2020 15:45:53 -0700 Subject: [PATCH 42/67] fix lint and type issues --- .../legacy/plugins/siem/public/containers/case/types.ts | 6 +++--- .../pages/case/components/description_md_editor/index.tsx | 8 +------- .../siem/server/lib/detection_engine/rules/find_rules.ts | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 8fc6b7161c4ab2..49c5f8eddce516 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -13,7 +13,7 @@ interface FormData { export interface NewCase extends FormData { description: string; - tags: string[] | []; + tags: string[]; title: string; case_type: string; } @@ -28,7 +28,7 @@ interface Case { created_by: ElasticUser; description: string; state: string; - tags: string[] | []; + tags: string[]; title: string; updated_at: number; } @@ -53,7 +53,7 @@ export interface CaseResult { created_by: ElasticUser; description: string; state: string; - tags: string[] | []; + tags: string[]; title: string; updated_at: number; } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx index a1eb8edb2d5d49..ecfa0f9a70c7ad 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem, EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; +import { EuiFlexItem, EuiPanel, EuiTabbedContent } from '@elastic/eui'; import React, { useState } from 'react'; import styled from 'styled-components'; @@ -31,12 +31,6 @@ const MarkdownContainer = styled(EuiPanel)<{ height: number }>` MarkdownContainer.displayName = 'MarkdownContainer'; -const TextArea = styled(EuiTextArea)<{ height: number }>` - min-height: ${({ height }) => `${height}px`}; - width: 100%; -`; -TextArea.displayName = 'TextArea'; - /** An input for entering a new case description */ export const DescriptionMarkdown = React.memo<{ descriptionInputHeight: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts index cdeddcb484cf66..e193e123f42817 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts @@ -16,7 +16,7 @@ export const getFilter = (filter: string | null | undefined) => { } }; -export const cli = async ({ +export const findRules = async ({ alertsClient, perPage, page, From fa87229fc35020f4fe13f59df52170f9b23ee7a5 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Wed, 5 Feb 2020 11:04:29 -0700 Subject: [PATCH 43/67] adding update hook --- .../containers/case/use_update_case.tsx | 95 ++++++++++++++++++ .../pages/case/components/all_cases/index.tsx | 3 +- .../case/components/all_cases/translations.ts | 34 ++----- .../pages/case/components/case_view/index.tsx | 99 +++++++++++++++---- .../pages/case/components/case_view/schema.ts | 24 +++++ .../case/components/create/form_options.ts | 27 +++++ .../pages/case/components/create/index.tsx | 12 +-- .../pages/case/components/create/schema.tsx | 38 ++----- .../siem/public/pages/case/translations.ts | 87 +++++++++++++--- 9 files changed, 315 insertions(+), 104 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx new file mode 100644 index 00000000000000..4ad6d099c73198 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -0,0 +1,95 @@ +/* + * 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 { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_CASE } from './constants'; +import { CaseSavedObject, NewCaseFormatted } from './types'; +import { createCase } from './api'; + +interface NewCaseState { + data: NewCaseFormatted; + newCase?: CaseSavedObject; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: NewCaseFormatted | CaseSavedObject; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + let getTypedPayload; + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case POST_NEW_CASE: + getTypedPayload = (a: Action['payload']) => a as NewCaseFormatted; + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_SUCCESS: + getTypedPayload = (a: Action['payload']) => a as CaseSavedObject; + return { + ...state, + isLoading: false, + isError: false, + newCase: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; + +export const useUpdateCase = ( + initialData: NewCaseFormatted +): [NewCaseState, Dispatch>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [formData, setFormData] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + dispatch({ type: POST_NEW_CASE, payload: formData }); + }, [formData]); + + useEffect(() => { + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const dataWithoutIsNew = state.data; + delete dataWithoutIsNew.isNew; + const response = await createCase(dataWithoutIsNew); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.data.isNew) { + fetchData(); + } + }, [state.data.isNew]); + return [state, setFormData]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 620d9af2599dc2..072d45684e2a1f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -35,6 +35,7 @@ import { UtilityBarSection, UtilityBarText, } from '../../../../components/detection_engine/utility_bar'; +import { getCreateCaseUrl } from '../../../../components/link_to'; export const AllCases = React.memo(() => { const [{ data, isLoading, pagination, filterOptions }, doFetch, setFilters] = useGetCases(); @@ -110,7 +111,7 @@ export const AllCases = React.memo(() => { titleSize="xs" body={i18n.NO_CASES_BODY} actions={ - + {i18n.ADD_NEW_CASE} } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index 7f13d9e83a8ac5..ab8e22ebcf1beb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -6,16 +6,18 @@ import { i18n } from '@kbn/i18n'; -export const ALL_CASES = i18n.translate('xpack.siem.caseTable.title', { +export * from '../../translations'; + +export const ALL_CASES = i18n.translate('xpack.siem.case.caseTable.title', { defaultMessage: 'All Cases', }); -export const NO_CASES = i18n.translate('xpack.siem.caseTable.noCases.title', { +export const NO_CASES = i18n.translate('xpack.siem.case.caseTable.noCases.title', { defaultMessage: 'No Cases', }); -export const NO_CASES_BODY = i18n.translate('xpack.siem.caseTable.noCases.body', { +export const NO_CASES_BODY = i18n.translate('xpack.siem.case.caseTable.noCases.body', { defaultMessage: 'Create a new case to see it displayed in the case workflow table.', }); -export const ADD_NEW_CASE = i18n.translate('xpack.siem.caseTable.addNewCase', { +export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase', { defaultMessage: 'Add New Case', }); @@ -31,30 +33,6 @@ export const UNIT = (totalCount: number) => defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, }); -export const CASE_TITLE = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { - defaultMessage: 'Case Title', -}); - -export const TAGS = i18n.translate('xpack.siem.case.caseTable.columnHeader.tags', { - defaultMessage: 'Tags', -}); - -export const CREATED_AT = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { - defaultMessage: 'Created at', -}); - -export const CREATED_BY = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { - defaultMessage: 'Created by', -}); - -export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { - defaultMessage: 'Last updated', -}); - -export const STATE = i18n.translate('xpack.siem.case.caseTable.columnHeader.caseTitle', { - defaultMessage: 'State', -}); - export const SEARCH_CASES = i18n.translate( 'xpack.siem.detectionEngine.case.caseTable.searchAriaLabel', { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 26d1f6052db08c..196170ea7c47f7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,9 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Fragment, useState } from 'react'; +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, +} from '@elastic/eui'; import { Markdown } from '../../../../components/markdown'; import { HeaderPage } from '../../../../components/header_page'; @@ -14,6 +21,12 @@ import * as i18n from '../../translations'; import { getCaseUrl } from '../../../../components/link_to'; import { useGetCase } from '../../../../containers/case/use_get_case'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { useForm } from '../shared_imports'; +import { schema } from './schema'; +import { DescriptionMarkdown } from '../description_md_editor'; +import { useUpdateCase } from '../../../../containers/case/use_update_case'; +import { CommonUseField } from '../create'; +import { caseTypeOptions, stateOptions } from '../create/form_options'; interface Props { caseId: string; @@ -26,8 +39,8 @@ const getDictionary = ( ) => { return definition ? ( -
{title}
-
{definition}
+ {title} + {definition}
) : null; }; @@ -36,33 +49,77 @@ export const CaseView = React.memo(({ caseId }: Props) => { if (isError) { return null; } + const [isEdit, setIsEdit] = useState(false); + const [{}, setFormData] = useUpdateCase(data); + const { form } = useForm({ + defaultValue: data, + options: { stripEmptyFields: false }, + schema, + }); const caseDetailsDefinitions = [ { - title: , + title: i18n.DESCRIPTION, + edit: ( + setFormData({ ...data, description })} + /> + ), definition: , }, { - title: , + title: i18n.CASE_TYPE, + edit: ( + + ), definition: data.case_type, }, { - title: , + title: i18n.STATE, + edit: ( + + ), definition: data.state, }, { - title: , + title: i18n.LAST_UPDATED, definition: , }, { - title: , + title: i18n.CREATED_AT, definition: , }, { - title: , + title: i18n.CREATED_BY, definition: data.created_by.username, }, { - title: , + title: i18n.TAGS, + edit: data.description, definition: data.tags.length > 0 ? (
    @@ -89,14 +146,16 @@ export const CaseView = React.memo(({ caseId }: Props) => { border subtitle={caseId} title={data.title} - /> - -
    - {caseDetailsDefinitions.map((dictionaryItem, key) => - getDictionary(dictionaryItem.title, dictionaryItem.definition, key) - )} -
    -
    + > + + {i18n.EDIT} + + + + {caseDetailsDefinitions.map((dictionaryItem, key) => + getDictionary(dictionaryItem.title, dictionaryItem.definition, key) + )} + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts new file mode 100644 index 00000000000000..6e02b7f31dfdbb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts @@ -0,0 +1,24 @@ +/* + * 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 { schema as initialCaseSchema } from '../create/schema'; + +import { FIELD_TYPES, fieldValidators, FormSchema } from '../shared_imports'; +import * as i18n from '../../translations'; +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + ...initialCaseSchema, + state: { + type: FIELD_TYPES.SUPER_SELECT, + label: i18n.STATE, + validations: [ + { + validator: emptyField(i18n.STATE_REQUIRED), + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts new file mode 100644 index 00000000000000..44c009b5667955 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export const caseTypeOptions = [ + { + value: 'security', + inputDisplay: 'Security', + }, + { + value: 'other', + inputDisplay: 'Other', + }, +]; + +export const stateOptions = [ + { + value: 'open', + inputDisplay: 'Open', + }, + { + value: 'closed', + inputDisplay: 'Closed', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 456c9098d07887..d575e735639949 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -21,6 +21,7 @@ import { schema } from './schema'; import * as i18n from '../../translations'; import { SiemPageName } from '../../../home/types'; import { DescriptionMarkdown } from '../description_md_editor'; +import { caseTypeOptions } from './form_options'; export const CommonUseField = getUseField({ component: Field }); @@ -33,17 +34,6 @@ const MySpinner = styled(EuiLoadingSpinner)` left: 50%; `; -const caseTypeOptions = [ - { - value: 'security', - inputDisplay: 'Security', - }, - { - value: 'other', - inputDisplay: 'Other', - }, -]; - export const Create = React.memo(() => { const [{ data, isLoading, newCase }, setFormData] = usePostCase(); const { form } = useForm({ diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx index 6132f297c39776..b754512466e5a2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx @@ -4,26 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; - import { FIELD_TYPES, fieldValidators, FormSchema } from '../shared_imports'; import { OptionalFieldLabel } from './optional_field_label'; +import * as i18n from '../../translations'; const { emptyField } = fieldValidators; export const schema: FormSchema = { title: { type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.siem.case.createCase.fieldTitleLabel', { - defaultMessage: 'Title', - }), + label: i18n.CASE_TITLE, validations: [ { - validator: emptyField( - i18n.translate('xpack.siem.case.createCase.titleFieldRequiredError', { - defaultMessage: 'A title is required.', - }) - ), + validator: emptyField(i18n.TITLE_REQUIRED), }, ], }, @@ -31,38 +24,23 @@ export const schema: FormSchema = { type: FIELD_TYPES.TEXTAREA, validations: [ { - validator: emptyField( - i18n.translate('xpack.siem.case.createCase.descriptionFieldRequiredError', { - defaultMessage: 'A description is required.', - }) - ), + validator: emptyField(i18n.DESCRIPTION_REQUIRED), }, ], }, case_type: { type: FIELD_TYPES.SUPER_SELECT, - label: i18n.translate('xpack.siem.case.createCase.fieldSeverityLabel', { - defaultMessage: 'Case Type', - }), + label: i18n.CASE_TYPE, validations: [ { - validator: emptyField( - i18n.translate('xpack.siem.case.createCase.caseTypeFieldRequiredError', { - defaultMessage: 'A case type is required.', - }) - ), + validator: emptyField(i18n.CASE_TYPE_REQUIRED), }, ], }, tags: { type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate('xpack.siem.case.createCase.fieldTagsLabel', { - defaultMessage: 'Tags', - }), - helpText: i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { - defaultMessage: - 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', - }), + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, labelAppend: OptionalFieldLabel, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index f5e770d2837dcd..04ec1e20166b60 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -6,24 +6,62 @@ import { i18n } from '@kbn/i18n'; -export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { - defaultMessage: 'Case Workflows', +export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', { + defaultMessage: 'Back to all cases', +}); + +export const CASE_TITLE = i18n.translate('xpack.siem.case.caseView.caseTitle', { + defaultMessage: 'Case Title', +}); + +export const CASE_TYPE = i18n.translate('xpack.siem.case.caseView.case_type', { + defaultMessage: 'Case type', +}); + +export const CASE_TYPE_REQUIRED = i18n.translate( + 'xpack.siem.case.createCase.caseTypeFieldRequiredError', + { + defaultMessage: 'A case type is required.', + } +); + +export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.created_at', { + defaultMessage: 'Created at', +}); + +export const CREATED_BY = i18n.translate('xpack.siem.case.caseView.created_by', { + defaultMessage: 'Created by', +}); + +export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.caseView.breadcrumb', { + defaultMessage: 'Create', }); export const CREATE_TITLE = i18n.translate('xpack.siem.case.create.title', { defaultMessage: 'Create new case', }); -export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.create.breadcrumb', { - defaultMessage: 'Create', +export const DESCRIPTION = i18n.translate('xpack.siem.case.casePage.description', { + defaultMessage: 'Description', }); -export const BACK_TO_ALL = i18n.translate('xpack.siem.case.back.label', { - defaultMessage: 'Back to all cases', +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.siem.case.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const EDIT = i18n.translate('xpack.siem.case.caseView.tags', { + defaultMessage: 'Edit', }); -export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.casePage.pageSubtitle', { - defaultMessage: 'Case Workflow Management within the Elastic SIEM', +export const OPTIONAL = i18n.translate('xpack.siem.case.casePage.optional', { + defaultMessage: 'Optional', +}); + +export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updated_at', { + defaultMessage: 'Last updated', }); export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.casePage.pageBadgeLabel', { @@ -35,18 +73,39 @@ export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.casePage.pageB 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', }); -export const OPTIONAL = i18n.translate('xpack.siem.case.casePage.optional', { - defaultMessage: 'Optional', +export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.casePage.pageSubtitle', { + defaultMessage: 'Case Workflow Management within the Elastic SIEM', +}); + +export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { + defaultMessage: 'Case Workflows', +}); + +export const PREVIEW = i18n.translate('xpack.siem.case.casePage.preview', { + defaultMessage: 'Preview', +}); + +export const STATE = i18n.translate('xpack.siem.case.caseView.state', { + defaultMessage: 'State', +}); + +export const STATE_REQUIRED = i18n.translate('xpack.siem.case.createCase.stateFieldRequiredError', { + defaultMessage: 'A case state is required.', }); export const SUBMIT = i18n.translate('xpack.siem.case.casePage.submit', { defaultMessage: 'Submit', }); -export const DESCRIPTION = i18n.translate('xpack.siem.case.casePage.description', { - defaultMessage: 'Description', +export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { + defaultMessage: 'Tags', }); -export const PREVIEW = i18n.translate('xpack.siem.case.casePage.preview', { - defaultMessage: 'Preview', +export const TAGS_HELP = i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', +}); + +export const TITLE_REQUIRED = i18n.translate('xpack.siem.case.createCase.titleFieldRequiredError', { + defaultMessage: 'A title is required.', }); From 6a8ca8a684d18b640e1a58116836db51507866e3 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 6 Feb 2020 08:38:00 -0700 Subject: [PATCH 44/67] update case added --- .../siem/public/containers/case/api.ts | 36 +++++++- .../siem/public/containers/case/constants.ts | 2 + .../public/containers/case/use_get_case.tsx | 67 +++++++++----- .../public/containers/case/use_get_cases.tsx | 4 +- .../containers/case/use_update_case.tsx | 15 ++-- .../pages/case/components/case_view/index.tsx | 88 +++++++++++++++---- .../siem/public/pages/case/translations.ts | 4 + 7 files changed, 165 insertions(+), 51 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 1ad25d0dbe923f..08f4bb4ec2c919 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -16,7 +16,23 @@ import { Direction } from '../../graphql/types'; import { throwIfNotOk } from '../../hooks/api/api'; import { CASES_URL } from './constants'; -export const fetchCases = async ({ +export const getCase = async (caseId: string, includeComments: boolean) => { + const response = await fetch( + `${chrome.getBasePath()}/api/cases/${caseId}?includeComments=${includeComments}`, + { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + }, + } + ); + await throwIfNotOk(response); + return response.json(); +}; + +export const getCases = async ({ filterOptions = { search: '', tags: [], @@ -69,3 +85,21 @@ export const createCase = async (newCase: NewCase): Promise => await throwIfNotOk(response); return response.json(); }; + +export const updateCase = async ( + caseId: string, + updatedCase: NewCaseFormatted +): Promise => { + const response = await fetch(`${chrome.getBasePath()}${CASES_URL}/${caseId}`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + 'kbn-xsrf': 'true', + }, + body: JSON.stringify(updatedCase), + }); + await throwIfNotOk(response); + return response.json(); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index 5c277a7b89bfad..f49ba5f255a090 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -10,6 +10,8 @@ export const FETCH_FAILURE = 'FETCH_FAILURE'; export const UPDATE_PAGINATION = 'UPDATE_PAGINATION'; export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; export const POST_NEW_CASE = 'POST_NEW_CASE'; +export const UPDATE_CASE = 'UPDATE_CASE'; +export const REFRESH_CASE = 'REFRESH_CASE'; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index 16b8268117bcc9..fb94d52f841b14 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useReducer, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; -import chrome from 'ui/chrome'; -import { FlattenedCaseSavedObject } from './types'; -import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; +import { FlattenedCaseSavedObject, NewCaseFormatted } from './types'; +import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS, REFRESH_CASE } from './constants'; import { flattenSavedObject } from './utils'; -import { throwIfNotOk } from '../../hooks/api/api'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; import { useStateToaster } from '../../components/toasters'; +import { getCase } from './api'; interface CaseState { data: FlattenedCaseSavedObject; @@ -22,10 +21,11 @@ interface CaseState { } interface Action { type: string; - payload?: FlattenedCaseSavedObject; + payload?: FlattenedCaseSavedObject | NewCaseFormatted; } const dataFetchReducer = (state: CaseState, action: Action): CaseState => { + let getTypedPayload; switch (action.type) { case FETCH_INIT: return { @@ -34,7 +34,7 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { isError: false, }; case FETCH_SUCCESS: - const getTypedPayload = (a: Action['payload']) => a as FlattenedCaseSavedObject; + getTypedPayload = (a: Action['payload']) => a as FlattenedCaseSavedObject; return { ...state, isLoading: false, @@ -47,6 +47,17 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { isLoading: false, isError: true, }; + case REFRESH_CASE: + getTypedPayload = (a: Action['payload']) => a as NewCaseFormatted; + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + ...getTypedPayload(action.payload), + }, + }; default: throw new Error(); } @@ -66,7 +77,16 @@ const initialData: FlattenedCaseSavedObject = { updated_at: 0, version: '', }; -export const useGetCase = (initialCaseId: string): [CaseState] => { +const initialRefreshData: NewCaseFormatted = { + case_type: '', + description: '', + state: '', + tags: [], + title: '', +}; +export const useGetCase = ( + initialCaseId: string +): [CaseState, Dispatch>] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, @@ -74,27 +94,16 @@ export const useGetCase = (initialCaseId: string): [CaseState] => { }); const [caseId, setCaseId] = useState(initialCaseId); const [, dispatchToaster] = useStateToaster(); + const [refreshData, refreshCase] = useState(initialRefreshData); - useEffect(() => { + const callFetch = () => { let didCancel = false; const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const response = await fetch( - `${chrome.getBasePath()}/api/cases/${caseId}?includeComments=false`, - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - }, - } - ); + const response = await getCase(caseId, false); if (!didCancel) { - await throwIfNotOk(response); - const responseJson = await response.json(); - dispatch({ type: FETCH_SUCCESS, payload: flattenSavedObject(responseJson) }); + dispatch({ type: FETCH_SUCCESS, payload: flattenSavedObject(response) }); } } catch (error) { if (!didCancel) { @@ -108,6 +117,16 @@ export const useGetCase = (initialCaseId: string): [CaseState] => { didCancel = true; setCaseId(initialCaseId); }; + }; + + useEffect(() => { + if (refreshData.description.length > 0) { + dispatch({ type: REFRESH_CASE, payload: refreshData }); + } + }, [refreshData]); + + useEffect(() => { + callFetch(); }, [caseId]); - return [state]; + return [state, refreshCase]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 8fb7c34ed72e30..236dc8eca44f38 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -28,7 +28,7 @@ import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { flattenSavedObjects } from './utils'; -import { fetchCases } from './api'; +import { getCases } from './api'; const dataFetchReducer = (state: CasesState, action: Action): CasesState => { let getTypedPayload; @@ -113,7 +113,7 @@ export const useGetCases = (): [ const fetchData = async () => { dispatch({ type: FETCH_INIT }); try { - const response = await fetchCases({ + const response = await getCases({ filterOptions: state.filterOptions, pagination: state.pagination, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 4ad6d099c73198..f565fe59ddbb73 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -8,9 +8,9 @@ import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react import { useStateToaster } from '../../components/toasters'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_CASE } from './constants'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, UPDATE_CASE } from './constants'; import { CaseSavedObject, NewCaseFormatted } from './types'; -import { createCase } from './api'; +import { updateCase } from './api'; interface NewCaseState { data: NewCaseFormatted; @@ -32,7 +32,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => isLoading: true, isError: false, }; - case POST_NEW_CASE: + case UPDATE_CASE: getTypedPayload = (a: Action['payload']) => a as NewCaseFormatted; return { ...state, @@ -60,8 +60,9 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => }; export const useUpdateCase = ( + caseId: string, initialData: NewCaseFormatted -): [NewCaseState, Dispatch>] => { +): [Dispatch>] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, @@ -71,7 +72,7 @@ export const useUpdateCase = ( const [, dispatchToaster] = useStateToaster(); useEffect(() => { - dispatch({ type: POST_NEW_CASE, payload: formData }); + dispatch({ type: UPDATE_CASE, payload: formData }); }, [formData]); useEffect(() => { @@ -80,7 +81,7 @@ export const useUpdateCase = ( try { const dataWithoutIsNew = state.data; delete dataWithoutIsNew.isNew; - const response = await createCase(dataWithoutIsNew); + const response = await updateCase(caseId, dataWithoutIsNew); dispatch({ type: FETCH_SUCCESS, payload: response }); } catch (error) { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); @@ -91,5 +92,5 @@ export const useUpdateCase = ( fetchData(); } }, [state.data.isNew]); - return [state, setFormData]; + return [setFormData]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 196170ea7c47f7..9e1ec1a7589f79 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState } from 'react'; +import React, { Fragment, useCallback, useState } from 'react'; import { + EuiButton, EuiButtonEmpty, EuiDescriptionList, EuiDescriptionListDescription, @@ -21,42 +22,61 @@ import * as i18n from '../../translations'; import { getCaseUrl } from '../../../../components/link_to'; import { useGetCase } from '../../../../containers/case/use_get_case'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; -import { useForm } from '../shared_imports'; +import { Form, useForm } from '../shared_imports'; import { schema } from './schema'; import { DescriptionMarkdown } from '../description_md_editor'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { CommonUseField } from '../create'; import { caseTypeOptions, stateOptions } from '../create/form_options'; +import { NewCaseFormatted } from '../../../../containers/case/types'; interface Props { caseId: string; } +interface CaseDetail { + title: React.ReactNode; + definition: string | number | JSX.Element | null; + edit?: JSX.Element; +} + const getDictionary = ( - title: React.ReactNode, - definition: string | number | JSX.Element | null, - key: number + { title, definition, edit }: CaseDetail, + key: number, + isEdit: boolean = false ) => { return definition ? ( - {title} - {definition} + {isEdit && edit ? null : {title}} + + {isEdit && edit ? edit : definition} + ) : null; }; export const CaseView = React.memo(({ caseId }: Props) => { - const [{ data, isLoading, isError }] = useGetCase(caseId); + const [{ data, isLoading, isError }, refreshCase] = useGetCase(caseId); if (isError) { return null; } const [isEdit, setIsEdit] = useState(false); - const [{}, setFormData] = useUpdateCase(data); + const [setFormData] = useUpdateCase(caseId, data); const { form } = useForm({ defaultValue: data, options: { stripEmptyFields: false }, schema, }); - const caseDetailsDefinitions = [ + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid) { + setFormData({ ...newData, isNew: true } as NewCaseFormatted); + refreshCase(newData as NewCaseFormatted); + setIsEdit(false); + } + }, [form]); + + const caseDetailsDefinitions: CaseDetail[] = [ { title: i18n.DESCRIPTION, edit: ( @@ -119,7 +139,20 @@ export const CaseView = React.memo(({ caseId }: Props) => { }, { title: i18n.TAGS, - edit: data.description, + edit: ( + + ), definition: data.tags.length > 0 ? (
      @@ -148,14 +181,35 @@ export const CaseView = React.memo(({ caseId }: Props) => { title={data.title} > - {i18n.EDIT} + {isEdit ? ( + + + {i18n.SUBMIT} + + + ) : null} + + setIsEdit(!isEdit)}> + {isEdit ? i18n.CANCEL : i18n.EDIT} + + - - {caseDetailsDefinitions.map((dictionaryItem, key) => - getDictionary(dictionaryItem.title, dictionaryItem.definition, key) - )} - + {isEdit ? ( +
      + + {caseDetailsDefinitions.map((dictionaryItem, key) => + getDictionary(dictionaryItem, key, isEdit) + )} + +
      + ) : ( + + {caseDetailsDefinitions.map((dictionaryItem, key) => + getDictionary(dictionaryItem, key, isEdit) + )} + + )} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 04ec1e20166b60..22d73f60816b8a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -10,6 +10,10 @@ export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', defaultMessage: 'Back to all cases', }); +export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { + defaultMessage: 'Cancel', +}); + export const CASE_TITLE = i18n.translate('xpack.siem.case.caseView.caseTitle', { defaultMessage: 'Case Title', }); From 3dd0f5a129c19b3bebad47fa1948c015a9e538d2 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 7 Feb 2020 08:02:04 -0700 Subject: [PATCH 45/67] working on user action tree component --- .../components/formatted_date/index.tsx | 17 +- .../public/components/header_global/index.tsx | 2 +- .../siem/public/pages/case/case_details.tsx | 10 +- .../pages/case/components/case_view/index.tsx | 170 +++++++++++++----- .../case/components/case_view/translations.ts | 31 ++++ .../components/user_action_tree/index.tsx | 80 +++++++++ .../siem/public/pages/case/translations.ts | 2 +- .../case/server/saved_object_mappings.ts | 95 ++++++++++ 8 files changed, 357 insertions(+), 50 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx create mode 100644 x-pack/plugins/case/server/saved_object_mappings.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index f74ee995c965b2..3452d0b289ac9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -8,6 +8,7 @@ import moment from 'moment-timezone'; import React from 'react'; import { FormattedRelative } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { useDateFormat, useTimeZone, useUiSetting$ } from '../../lib/kibana'; import { getOrEmptyTagFromValue } from '../empty_value'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; @@ -104,7 +105,13 @@ FormattedDate.displayName = 'FormattedDate'; * - the raw date value (e.g. 2019-03-22T00:47:46Z) */ -export const FormattedRelativePreferenceDate = ({ value }: { value?: string | number | null }) => { +export const FormattedRelativePreferenceDate = ({ + value, + labelOn = false, +}: { + value?: string | number | null; + labelOn?: boolean; +}) => { if (value == null) { return getOrEmptyTagFromValue(value); } @@ -118,7 +125,13 @@ export const FormattedRelativePreferenceDate = ({ value }: { value?: string | nu {moment(date) .add(1, 'hours') .isBefore(new Date()) ? ( - + <> + {labelOn && + i18n.translate('xpack.siem.alertsView.alertsGraphTitle', { + defaultMessage: 'on ', + })} + + ) : ( )} diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx index a12fab8a4f5d92..da29e12b1eb497 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx @@ -81,7 +81,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine {i18n.BUTTON_ADD_DATA} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index f12d996ab49ad5..651fc1596bd980 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -17,11 +17,11 @@ interface Props { export const CaseDetailsPage = React.memo(({ caseId }: Props) => ( <> - - - - - + {/* */} + {/* */} + + {/* */} + {/* */} )); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 9e1ec1a7589f79..a79e086d8839a9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -6,6 +6,7 @@ import React, { Fragment, useCallback, useState } from 'react'; import { + EuiAvatar, EuiButton, EuiButtonEmpty, EuiDescriptionList, @@ -14,11 +15,16 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, + EuiPanel, + EuiSteps, + EuiText, } from '@elastic/eui'; +import styled, { css } from 'styled-components'; import { Markdown } from '../../../../components/markdown'; import { HeaderPage } from '../../../../components/header_page'; -import * as i18n from '../../translations'; +import { WrapperPage } from '../../../../components/wrapper_page'; +import * as i18n from './translations'; import { getCaseUrl } from '../../../../components/link_to'; import { useGetCase } from '../../../../containers/case/use_get_case'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -29,6 +35,7 @@ import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { CommonUseField } from '../create'; import { caseTypeOptions, stateOptions } from '../create/form_options'; import { NewCaseFormatted } from '../../../../containers/case/types'; +import { UserActionTree } from '../user_action_tree'; interface Props { caseId: string; @@ -40,6 +47,45 @@ interface CaseDetail { edit?: JSX.Element; } +const MyWrapper = styled(WrapperPage)` + padding-bottom: 0; + ${({ theme }) => css` + @media only screen and (min-width: ${theme.eui.euiBreakpoints.l}) { + margin: 0 auto; + width: 85%; + } + `} +`; +const BackgroundWrapper = styled.div` + ${({ theme }) => css` + background-color: ${theme.eui.euiSizeM}; + `} +`; + +const MySteps = styled(EuiSteps)` + ${({ stepAuthor, theme }) => css` + .euiStepNumber::before { + content: ${() => }; + } + p.euiTitle { + width: 100%; + padding-right: 16px; + .euiPanel { + background-color: ${theme.eui.euiColorLightestShade}; + border-bottom: none; + border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0; + } + } + .euiStep__content { + padding-top: 0; + margin-top: 0; + .euiPanel { + border-radius: 0 0 ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius}; + } + } + `} +`; + const getDictionary = ( { title, definition, edit }: CaseDetail, key: number, @@ -75,7 +121,43 @@ export const CaseView = React.memo(({ caseId }: Props) => { setIsEdit(false); } }, [form]); - + const firstSetOfSteps = [ + { + avatarName: data.created_by.username, + title: ( + +

      + {`${data.created_by.username}`} + {` ${i18n.ADDED_DESCRIPTION} `}{' '} + +

      +
      + ), + children: isEdit ? ( + setFormData({ ...data, description })} + /> + ) : ( + + ), + }, + { + avatarName: `steph`, + title: ( + +

      + {`${data.created_by.username}`} + {` ${i18n.ADDED_DESCRIPTION} `}{' '} + +

      +
      + ), + children:

      {'alright alright alright'}

      , + }, + ]; const caseDetailsDefinitions: CaseDetail[] = [ { title: i18n.DESCRIPTION, @@ -170,47 +252,53 @@ export const CaseView = React.memo(({ caseId }: Props) => { ) : ( - - - - {isEdit ? ( + <> + + + + {isEdit ? ( + + + {i18n.SUBMIT} + + + ) : null} - - {i18n.SUBMIT} - + setIsEdit(!isEdit)}> + {isEdit ? i18n.CANCEL : i18n.EDIT} + - ) : null} - - setIsEdit(!isEdit)}> - {isEdit ? i18n.CANCEL : i18n.EDIT} - - - - - {isEdit ? ( -
      - - {caseDetailsDefinitions.map((dictionaryItem, key) => - getDictionary(dictionaryItem, key, isEdit) - )} - -
      - ) : ( - - {caseDetailsDefinitions.map((dictionaryItem, key) => - getDictionary(dictionaryItem, key, isEdit) - )} - - )} -
      + + + + + + + {/* */} + {/* {isEdit ? (*/} + {/*
      */} + {/* */} + {/* {caseDetailsDefinitions.map((dictionaryItem, key) =>*/} + {/* getDictionary(dictionaryItem, key, isEdit)*/} + {/* )}*/} + {/* */} + {/*
      */} + {/* ) : (*/} + {/* */} + {/* {caseDetailsDefinitions.map((dictionaryItem, key) =>*/} + {/* getDictionary(dictionaryItem, key, isEdit)*/} + {/* )}*/} + {/* */} + {/* )}*/} +
      +
      + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts new file mode 100644 index 00000000000000..191550199174fc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -0,0 +1,31 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const SHOWING_CASES = (actionDate: string, actionName: string, userName: string) => + i18n.translate('xpack.siem.case.caseView.actionHeadline', { + values: { + actionDate, + actionName, + userName, + }, + defaultMessage: '{userName} {actionName} on {actionDate}', + }); + +export const ADDED_DESCRIPTION = i18n.translate('xpack.siem.case.caseTable.noCases.body', { + defaultMessage: 'added description', +}); + +export const ADDED_TAGS = i18n.translate('xpack.siem.case.caseTable.noCases.body', { + defaultMessage: 'added tags', +}); + +export const CHANGED_STATE = i18n.translate('xpack.siem.case.caseTable.noCases.body', { + defaultMessage: 'changed state', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx new file mode 100644 index 00000000000000..4652c24c378951 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -0,0 +1,80 @@ +/* + * 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, { ReactNode } from 'react'; +import { EuiAvatar, EuiPanel, EuiTitle } from '@elastic/eui'; +import styled, { css } from 'styled-components'; +export interface UserActionItem { + avatarName: string; + children: ReactNode; + title: string; +} + +export interface UserActionTreeProps { + userActions: UserActionItem[]; +} + +const UserAction = styled.div` + ${({ theme }) => css` + &:not(:last-of-type) { + background-image: linear-gradient( + to right, + transparent 0, + transparent 15px, + ${theme.eui.euiBorderColor} 15px, + ${theme.eui.euiBorderColor} 17px, + transparent 17px, + transparent 100% + ); + background-repeat: no-repeat; + background-position: left ${theme.eui.euiSizeXXL}; + } + .userAction__titleWrapper { + display: flex; + } + .userAction__circle { + flex-shrink: 0; + margin-right: ${theme.eui.euiSize}; + vertical-align: top; + } + .userAction__title { + } + .userAction__content { + padding: ${theme.eui.euiSize} ${theme.eui.euiSize} ${theme.eui.euiSizeXL}; + margin: ${theme.eui.euiSizeS} 0; + + // Align the content's contents with the title + padding-left: 24px; + + // Align content border to horizontal center of step number + margin-left: 16px; + } + `} +`; + +const renderUserActions = (userActions: UserActionItem[]) => { + return userActions.map(({ avatarName, children, title }, key) => ( + +
      + + + +

      {title}

      +
      +
      +
      +
      + {children} +
      +
      + )); +}; + +export const UserActionTree = React.memo(({ userActions }: UserActionTreeProps) => ( +
      {renderUserActions(userActions)}
      +)); + +UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 22d73f60816b8a..6d50148a4e8664 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', { - defaultMessage: 'Back to all cases', + defaultMessage: 'Back to cases', }); export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { diff --git a/x-pack/plugins/case/server/saved_object_mappings.ts b/x-pack/plugins/case/server/saved_object_mappings.ts new file mode 100644 index 00000000000000..5309f5fa4b4ef6 --- /dev/null +++ b/x-pack/plugins/case/server/saved_object_mappings.ts @@ -0,0 +1,95 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/camelcase */ +import { SavedObjectsType } from 'kibana/server'; +import { NewCaseFormatted, NewCommentFormatted } from './'; +import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; + +// Temporary file to write mappings for case +// while Saved Object Mappings API is programmed for the NP +// See: https://github.com/elastic/kibana/issues/50309 + +export const caseSavedObjectType = 'case-workflow'; +export const caseCommentSavedObjectType = 'case-workflow-comment'; +export const caseSavedObjectMappings: { + [caseSavedObjectType]: ElasticsearchMappingOf; +} = { + [caseSavedObjectType]: { + properties: { + created_at: { + type: 'date', + }, + description: { + type: 'text', + }, + title: { + type: 'keyword', + }, + created_by: { + properties: { + username: { + type: 'keyword', + }, + full_name: { + type: 'keyword', + }, + }, + }, + state: { + type: 'keyword', + }, + tags: { + type: 'keyword', + }, + case_type: { + type: 'keyword', + }, + updated_at: { + type: 'date', + }, + }, + }, +}; + +export const caseCommentSavedObjectMappings: { + [caseCommentSavedObjectType]: ElasticsearchMappingOf; +} = { + [caseCommentSavedObjectType]: { + properties: { + comment: { + type: 'text', + }, + created_at: { + type: 'date', + }, + created_by: { + properties: { + full_name: { + type: 'keyword', + }, + username: { + type: 'keyword', + }, + }, + }, + }, + }, +}; + +export const caseSavedObjectConfig: SavedObjectsType = { + name: caseSavedObjectType, + hidden: false, + namespaceAgnostic: false, + mappings: caseSavedObjectMappings, +}; + +export const caseCommentSavedObjectConfig: SavedObjectsType = { + name: caseCommentSavedObjectType, + hidden: false, + namespaceAgnostic: false, + mappings: caseCommentSavedObjectMappings, +}; From db634f07071b927ee5c4a26587ebcfadd96ea4b3 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 7 Feb 2020 08:40:13 -0700 Subject: [PATCH 46/67] user action tree --- .../pages/case/components/case_view/index.tsx | 29 ----------- .../components/user_action_tree/index.tsx | 48 +++++++++---------- 2 files changed, 24 insertions(+), 53 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index a79e086d8839a9..ff85a222b9f021 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -6,17 +6,13 @@ import React, { Fragment, useCallback, useState } from 'react'; import { - EuiAvatar, EuiButton, EuiButtonEmpty, - EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - EuiPanel, - EuiSteps, EuiText, } from '@elastic/eui'; @@ -62,30 +58,6 @@ const BackgroundWrapper = styled.div` `} `; -const MySteps = styled(EuiSteps)` - ${({ stepAuthor, theme }) => css` - .euiStepNumber::before { - content: ${() => }; - } - p.euiTitle { - width: 100%; - padding-right: 16px; - .euiPanel { - background-color: ${theme.eui.euiColorLightestShade}; - border-bottom: none; - border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0; - } - } - .euiStep__content { - padding-top: 0; - margin-top: 0; - .euiPanel { - border-radius: 0 0 ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius}; - } - } - `} -`; - const getDictionary = ( { title, definition, edit }: CaseDetail, key: number, @@ -280,7 +252,6 @@ export const CaseView = React.memo(({ caseId }: Props) => { - {/* */} {/* {isEdit ? (*/} {/*
      */} {/* */} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index 4652c24c378951..1b859c9ae59877 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -5,21 +5,22 @@ */ import React, { ReactNode } from 'react'; -import { EuiAvatar, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiAvatar, EuiPanel, EuiTitle } from '@elastic/eui'; import styled, { css } from 'styled-components'; + export interface UserActionItem { avatarName: string; children: ReactNode; - title: string; + title: ReactNode; } export interface UserActionTreeProps { userActions: UserActionItem[]; } -const UserAction = styled.div` +const UserAction = styled(EuiFlexGroup)` ${({ theme }) => css` - &:not(:last-of-type) { + & { background-image: linear-gradient( to right, transparent 0, @@ -31,9 +32,10 @@ const UserAction = styled.div` ); background-repeat: no-repeat; background-position: left ${theme.eui.euiSizeXXL}; + margin-bottom: ${theme.eui.euiSizeS}; } - .userAction__titleWrapper { - display: flex; + .userAction__panel { + margin-bottom: ${theme.eui.euiSize}; } .userAction__circle { flex-shrink: 0; @@ -41,34 +43,32 @@ const UserAction = styled.div` vertical-align: top; } .userAction__title { + padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL}; + background: ${theme.eui.euiColorLightestShade}; + border-bottom: ${theme.eui.euiBorderThin}; } .userAction__content { - padding: ${theme.eui.euiSize} ${theme.eui.euiSize} ${theme.eui.euiSizeXL}; - margin: ${theme.eui.euiSizeS} 0; - - // Align the content's contents with the title - padding-left: 24px; - - // Align content border to horizontal center of step number - margin-left: 16px; + padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; } `} `; const renderUserActions = (userActions: UserActionItem[]) => { return userActions.map(({ avatarName, children, title }, key) => ( - -
      + + - - -

      {title}

      -
      +
      + + +
      + +

      {title}

      +
      +
      +
      {children}
      -
      -
      - {children} -
      +
      )); }; From 810e3b1a0b6f7513fb2f10373a87733437b85972 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Sat, 8 Feb 2020 09:29:03 -0700 Subject: [PATCH 47/67] case details --- .../case/components/all_cases/columns.tsx | 2 +- .../pages/case/components/case_view/index.tsx | 55 +++++++++----- .../case/components/case_view/translations.ts | 22 +++--- .../pages/case/components/tag_list/index.tsx | 40 +++++++++++ .../components/user_action_tree/index.tsx | 18 ++--- .../pages/case/components/user_list/index.tsx | 72 +++++++++++++++++++ .../siem/public/pages/case/translations.ts | 4 +- .../plugins/siem/public/pages/home/index.tsx | 9 ++- 8 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index d625ec18590dcd..e96c6c17870991 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -60,7 +60,7 @@ export const getCasesColumns = (): CasesColumns[] => [ }, { field: 'created_by.username', - name: i18n.CREATED_BY, + name: i18n.REPORTER, render: (createdBy: FlattenedCaseSavedObject['created_by']['username']) => renderStringField(createdBy), }, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index ff85a222b9f021..182a464e310835 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -8,6 +8,7 @@ import React, { Fragment, useCallback, useState } from 'react'; import { EuiButton, EuiButtonEmpty, + EuiButtonIcon, EuiDescriptionListDescription, EuiDescriptionListTitle, EuiFlexGroup, @@ -32,6 +33,8 @@ import { CommonUseField } from '../create'; import { caseTypeOptions, stateOptions } from '../create/form_options'; import { NewCaseFormatted } from '../../../../containers/case/types'; import { UserActionTree } from '../user_action_tree'; +import { UserList } from '../user_list'; +import { TagList } from '../tag_list'; interface Props { caseId: string; @@ -54,7 +57,9 @@ const MyWrapper = styled(WrapperPage)` `; const BackgroundWrapper = styled.div` ${({ theme }) => css` - background-color: ${theme.eui.euiSizeM}; + background-color: ${theme.eui.euiColorEmptyShade}; + border-top: ${theme.eui.euiBorderThin}; + height: 100%; `} `; @@ -97,13 +102,22 @@ export const CaseView = React.memo(({ caseId }: Props) => { { avatarName: data.created_by.username, title: ( - -

      - {`${data.created_by.username}`} - {` ${i18n.ADDED_DESCRIPTION} `}{' '} - -

      -
      + + +

      + {`${data.created_by.username}`} + {` ${i18n.ADDED_DESCRIPTION} `}{' '} + +

      +
      + + window.alert('Description actions')} + iconType="boxesHorizontal" + aria-label="description actions" + /> + +
      ), children: isEdit ? ( { { avatarName: `steph`, title: ( - -

      - {`${data.created_by.username}`} - {` ${i18n.ADDED_DESCRIPTION} `}{' '} - -

      -
      +

      + {`steph`} + {` ${i18n.ADDED_COMMENT} `}{' '} + +

      ), children:

      {'alright alright alright'}

      , }, @@ -188,7 +200,7 @@ export const CaseView = React.memo(({ caseId }: Props) => { definition: , }, { - title: i18n.CREATED_BY, + title: i18n.REPORTER, definition: data.created_by.username, }, { @@ -251,7 +263,16 @@ export const CaseView = React.memo(({ caseId }: Props) => { - + + + + + + + + + + {/* {isEdit ? (*/} {/* */} {/* */} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index 191550199174fc..31637de9886bfc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -18,14 +18,20 @@ export const SHOWING_CASES = (actionDate: string, actionName: string, userName: defaultMessage: '{userName} {actionName} on {actionDate}', }); -export const ADDED_DESCRIPTION = i18n.translate('xpack.siem.case.caseTable.noCases.body', { - defaultMessage: 'added description', -}); +export const ADDED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.addDescription', + { + defaultMessage: 'added description', + } +); -export const ADDED_TAGS = i18n.translate('xpack.siem.case.caseTable.noCases.body', { - defaultMessage: 'added tags', -}); +export const EDITED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.editDescription', + { + defaultMessage: 'edited description', + } +); -export const CHANGED_STATE = i18n.translate('xpack.siem.case.caseTable.noCases.body', { - defaultMessage: 'changed state', +export const ADDED_COMMENT = i18n.translate('xpack.siem.case.caseView.actionLabel.addComment', { + defaultMessage: 'added comment', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx new file mode 100644 index 00000000000000..9d4ae3fd9d39c3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -0,0 +1,40 @@ +/* + * 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 from 'react'; +import { EuiText, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import * as i18n from '../../translations'; + +interface TagListProps { + tags: string[]; +} + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + `} +`; + +const renderTags = (tags: string[]) => { + return tags.map((tag, key) => ( + + {tag} + + )); +}; + +export const TagList = React.memo(({ tags }: TagListProps) => { + return ( + +

      {i18n.TAGS}

      + + {renderTags(tags)} +
      + ); +}); + +TagList.displayName = 'TagList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index 1b859c9ae59877..8df98a4cef0e8a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -5,12 +5,12 @@ */ import React, { ReactNode } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiAvatar, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiAvatar, EuiPanel, EuiText } from '@elastic/eui'; import styled, { css } from 'styled-components'; export interface UserActionItem { avatarName: string; - children: ReactNode; + children?: ReactNode; title: ReactNode; } @@ -46,10 +46,14 @@ const UserAction = styled(EuiFlexGroup)` padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL}; background: ${theme.eui.euiColorLightestShade}; border-bottom: ${theme.eui.euiBorderThin}; + border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0; } .userAction__content { padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; } + .euiText--small * { + margin-bottom: 0; + } `} `; @@ -61,12 +65,10 @@ const renderUserActions = (userActions: UserActionItem[]) => { -
      - -

      {title}

      -
      -
      -
      {children}
      + + {title} + + {children &&
      {children}
      }
      diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx new file mode 100644 index 00000000000000..b80ee58f8abbfe --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -0,0 +1,72 @@ +/* + * 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 from 'react'; +import { + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { ElasticUser } from '../../../../containers/case/types'; + +interface UserListProps { + headline: string; + users: ElasticUser[]; +} + +const MyAvatar = styled(EuiAvatar)` + top: -4px; +`; + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + `} +`; + +const renderUsers = (users: ElasticUser[]) => { + return users.map(({ username }, key) => ( + + + + + + + +

      + + {username} + +

      +
      +
      +
      + + window.alert('Email clicked')} + iconType="email" + aria-label="email" + /> + +
      + )); +}; + +export const UserList = React.memo(({ headline, users }: UserListProps) => { + return ( + +

      {headline}

      + + {renderUsers(users)} +
      + ); +}); + +UserList.displayName = 'UserList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 6d50148a4e8664..55a67b9c31c30c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -33,8 +33,8 @@ export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.created_at', defaultMessage: 'Created at', }); -export const CREATED_BY = i18n.translate('xpack.siem.case.caseView.created_by', { - defaultMessage: 'Created by', +export const REPORTER = i18n.translate('xpack.siem.case.caseView.created_by', { + defaultMessage: 'Reporter', }); export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.caseView.breadcrumb', { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 6c65078acf61b0..ad0ca421a14d03 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -43,6 +43,11 @@ const WrappedByAutoSizer = styled.div` `; WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +const Main = styled.main` + height: 100%; +`; +Main.displayName = 'Main'; + const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) /** the global Kibana navigation at the top of every page */ @@ -62,7 +67,7 @@ export const HomePage: React.FC = () => ( -
      +
      {({ browserFields, indexPattern, indicesExist }) => ( @@ -141,7 +146,7 @@ export const HomePage: React.FC = () => ( )} -
      +
      From a8c935ca2b7b3b340dd297d5ba2159c2be0ef5fb Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Sat, 8 Feb 2020 12:34:55 -0700 Subject: [PATCH 48/67] new header --- .../components/header_page_new/index.test.tsx | 224 +++++++++++++++ .../components/header_page_new/index.tsx | 164 +++++++++++ .../pages/case/components/case_view/index.tsx | 266 ++++++++---------- .../case/components/case_view/translations.ts | 8 + .../components/property_actions/constants.ts | 7 + .../components/property_actions/index.tsx | 116 ++++++++ 6 files changed, 632 insertions(+), 153 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx new file mode 100644 index 00000000000000..83a70fd90d82b5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx @@ -0,0 +1,224 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../mock'; +import { HeaderPage } from './index'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +describe('HeaderPage', () => { + const mount = useMountAppended(); + + test('it renders', () => { + const wrapper = shallow( + +

      {'Test supplement'}

      +
      + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the title', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-title"]') + .first() + .exists() + ).toBe(true); + }); + + test('it renders the back link when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the back link when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the first subtitle when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the first subtitle when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the second subtitle when provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle-2"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the second subtitle when not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle-2"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders supplements when children provided', () => { + const wrapper = mount( + + +

      {'Test supplement'}

      +
      +
      + ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render supplements when children not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(false); + }); + + test('it applies border styles when border is true', () => { + const wrapper = mount( + + + + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it DOES NOT apply border styles when border is false', () => { + const wrapper = mount( + + + + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it renders as a draggable when arguments provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render as a draggable when arguments not provided', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx new file mode 100644 index 00000000000000..22e316a9fb6703 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx @@ -0,0 +1,164 @@ +/* + * 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 { + EuiBetaBadge, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { DefaultDraggable } from '../draggables'; +import { LinkIcon, LinkIconProps } from '../link_icon'; +import { Subtitle, SubtitleProps } from '../subtitle'; + +interface HeaderProps { + border?: boolean; + isLoading?: boolean; +} + +const Header = styled.header.attrs({ + className: 'siemHeaderPage', +})` + ${({ border, theme }) => css` + margin-bottom: ${theme.eui.euiSizeL}; + + ${border && + css` + border-bottom: ${theme.eui.euiBorderThin}; + padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } + `} + `} +`; +Header.displayName = 'Header'; + +const FlexItem = styled(EuiFlexItem)` + display: block; +`; +FlexItem.displayName = 'FlexItem'; + +const LinkBack = styled.div.attrs({ + className: 'siemHeaderPage__linkBack', +})` + ${({ theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + margin-bottom: ${theme.eui.euiSizeS}; + `} +`; +LinkBack.displayName = 'LinkBack'; + +const Badge = styled(EuiBadge)` + letter-spacing: 0; +`; +Badge.displayName = 'Badge'; + +const StyledEuiBetaBadge = styled(EuiBetaBadge)` + vertical-align: middle; +`; + +StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; + +interface BackOptions { + href: LinkIconProps['href']; + text: LinkIconProps['children']; +} + +interface BadgeOptions { + beta?: boolean; + text: string; + tooltip?: string; +} + +interface DraggableArguments { + field: string; + value: string; +} + +export interface HeaderPageProps extends HeaderProps { + backOptions?: BackOptions; + badgeOptions?: BadgeOptions; + children?: React.ReactNode; + draggableArguments?: DraggableArguments; + subtitle?: SubtitleProps['items']; + subtitle2?: SubtitleProps['items']; + title: string | React.ReactNode; +} + +const HeaderPageComponent: React.FC = ({ + backOptions, + badgeOptions, + border, + children, + draggableArguments, + isLoading, + subtitle, + subtitle2, + title, + ...rest +}) => ( +
      + + + {backOptions && ( + + + {backOptions.text} + + + )} + + +

      + {!draggableArguments ? ( + title + ) : ( + + )} + {badgeOptions && ( + <> + {' '} + {badgeOptions.beta ? ( + + ) : ( + {badgeOptions.text} + )} + + )} +

      +
      + + {subtitle && } + {subtitle2 && } + {border && isLoading && } +
      + + {children && ( + + {children} + + )} +
      +
      +); + +export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 182a464e310835..49cc8fa8450dc5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,22 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiButton, EuiButtonEmpty, - EuiButtonIcon, EuiDescriptionListDescription, EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - EuiText, + EuiDescriptionList, + EuiButtonToggle, + EuiBadge, } from '@elastic/eui'; import styled, { css } from 'styled-components'; import { Markdown } from '../../../../components/markdown'; -import { HeaderPage } from '../../../../components/header_page'; +import { HeaderPage } from '../../../../components/header_page_new'; import { WrapperPage } from '../../../../components/wrapper_page'; import * as i18n from './translations'; import { getCaseUrl } from '../../../../components/link_to'; @@ -29,22 +30,24 @@ import { Form, useForm } from '../shared_imports'; import { schema } from './schema'; import { DescriptionMarkdown } from '../description_md_editor'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; -import { CommonUseField } from '../create'; -import { caseTypeOptions, stateOptions } from '../create/form_options'; import { NewCaseFormatted } from '../../../../containers/case/types'; import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { TagList } from '../tag_list'; +import { PropertyActions } from '../property_actions'; interface Props { caseId: string; } -interface CaseDetail { - title: React.ReactNode; - definition: string | number | JSX.Element | null; - edit?: JSX.Element; -} +const MyDescriptionList = styled(EuiDescriptionList)` + ${({ theme }) => css` + & { + padding-right: ${theme.eui.euiSizeL}; + border-right: ${theme.eui.euiBorderThin}; + } + `} +`; const MyWrapper = styled(WrapperPage)` padding-bottom: 0; @@ -63,32 +66,19 @@ const BackgroundWrapper = styled.div` `} `; -const getDictionary = ( - { title, definition, edit }: CaseDetail, - key: number, - isEdit: boolean = false -) => { - return definition ? ( - - {isEdit && edit ? null : {title}} - - {isEdit && edit ? edit : definition} - - - ) : null; -}; export const CaseView = React.memo(({ caseId }: Props) => { const [{ data, isLoading, isError }, refreshCase] = useGetCase(caseId); if (isError) { return null; } - const [isEdit, setIsEdit] = useState(false); const [setFormData] = useUpdateCase(caseId, data); const { form } = useForm({ defaultValue: data, options: { stripEmptyFields: false }, schema, }); + const [isEdit, setIsEdit] = useState(false); + const [isCaseOpen, setIsCaseOpen] = useState(data.state.toLowerCase() === 'open'); const onSubmit = useCallback(async () => { const { isValid, data: newData } = await form.submit(); @@ -98,7 +88,39 @@ export const CaseView = React.memo(({ caseId }: Props) => { setIsEdit(false); } }, [form]); - const firstSetOfSteps = [ + const propertyActions = [ + { + iconType: 'documentEdit', + label: 'Edit description', + onClick: () => setIsEdit(true), + }, + { + iconType: 'securitySignalResolved', + label: 'Close case', + onClick: () => null, + }, + { + iconType: 'trash', + label: 'Delete case', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Push as ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'popout', + label: 'View ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Update ServiceNow incident', + onClick: () => null, + }, + ]; + const userActions = [ { avatarName: data.created_by.username, title: ( @@ -111,21 +133,30 @@ export const CaseView = React.memo(({ caseId }: Props) => {

      - window.alert('Description actions')} - iconType="boxesHorizontal" - aria-label="description actions" - /> + ), children: isEdit ? ( - setFormData({ ...data, description })} - /> + <> + setFormData({ ...data, description })} + /> + + + + + {i18n.SUBMIT} + + + + setIsEdit(false)}>{i18n.CANCEL} + + + ) : ( ), @@ -142,93 +173,11 @@ export const CaseView = React.memo(({ caseId }: Props) => { children:

      {'alright alright alright'}

      , }, ]; - const caseDetailsDefinitions: CaseDetail[] = [ - { - title: i18n.DESCRIPTION, - edit: ( - setFormData({ ...data, description })} - /> - ), - definition: , - }, - { - title: i18n.CASE_TYPE, - edit: ( - - ), - definition: data.case_type, - }, - { - title: i18n.STATE, - edit: ( - - ), - definition: data.state, - }, - { - title: i18n.LAST_UPDATED, - definition: , - }, - { - title: i18n.CREATED_AT, - definition: , - }, - { - title: i18n.REPORTER, - definition: data.created_by.username, - }, - { - title: i18n.TAGS, - edit: ( - - ), - definition: - data.tags.length > 0 ? ( -
        - {data.tags.map((tag: string, key: number) => ( -
      • {tag}
      • - ))} -
      - ) : null, - }, - ]; + + useEffect(() => { + setIsCaseOpen(data.state.toLowerCase() === 'open'); + }, [data.state]); + return isLoading ? ( @@ -245,18 +194,39 @@ export const CaseView = React.memo(({ caseId }: Props) => { }} title={data.title} > - - {isEdit ? ( - - - {i18n.SUBMIT} - - - ) : null} + - setIsEdit(!isEdit)}> - {isEdit ? i18n.CANCEL : i18n.EDIT} - + + + + {i18n.STATUS} + + {data.state} + + + + {i18n.CASE_OPENED} + + + + + + + + + + + setIsCaseOpen(!isCaseOpen)} + isSelected={isCaseOpen} + /> + + + + + @@ -265,29 +235,19 @@ export const CaseView = React.memo(({ caseId }: Props) => { - + {isEdit ? ( + + + + ) : ( + + )} - - {/* {isEdit ? (*/} - {/*
      */} - {/* */} - {/* {caseDetailsDefinitions.map((dictionaryItem, key) =>*/} - {/* getDictionary(dictionaryItem, key, isEdit)*/} - {/* )}*/} - {/* */} - {/*
      */} - {/* ) : (*/} - {/* */} - {/* {caseDetailsDefinitions.map((dictionaryItem, key) =>*/} - {/* getDictionary(dictionaryItem, key, isEdit)*/} - {/* )}*/} - {/* */} - {/* )}*/}
      diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index 31637de9886bfc..f45c52533d2e7b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -35,3 +35,11 @@ export const EDITED_DESCRIPTION = i18n.translate( export const ADDED_COMMENT = i18n.translate('xpack.siem.case.caseView.actionLabel.addComment', { defaultMessage: 'added comment', }); + +export const STATUS = i18n.translate('xpack.siem.case.caseView.statusLabel', { + defaultMessage: 'Status', +}); + +export const CASE_OPENED = i18n.translate('xpack.siem.case.caseView.caseOpened', { + defaultMessage: 'Case opened', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts new file mode 100644 index 00000000000000..14e4b46eb83f04 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export const SET_STATE = 'SET_STATE'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx new file mode 100644 index 00000000000000..7cbfd77d006ee3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx @@ -0,0 +1,116 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPopover, EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; + +export interface PropertyActionButtonProps { + onClick: () => void; + iconType: string; + label: string; +} + +const PropertyActionButton = React.memo( + ({ onClick, iconType, label }) => ( + + {label} + + ) +); + +PropertyActionButton.displayName = 'PropertyActionButton'; + +export interface PropertyActionsProps { + propertyActions: PropertyActionButtonProps[]; +} + +// const propertyActions = [ +// { +// iconType: 'documentEdit', +// label: 'Edit description', +// onClick: () => null, +// }, +// { +// iconType: 'securitySignalResolved', +// label: 'Close case', +// onClick: () => null, +// }, +// { +// iconType: 'trash', +// label: 'Delete case', +// onClick: () => null, +// }, +// { +// iconType: 'importAction', +// label: 'Push as ServiceNow incident', +// onClick: () => null, +// }, +// { +// iconType: 'popout', +// label: 'View ServiceNow incident', +// onClick: () => null, +// }, +// { +// iconType: 'importAction', +// label: 'Update ServiceNow incident', +// onClick: () => null, +// }, +// ]; + +export const PropertyActions = React.memo(({ propertyActions }) => { + const [showActions, setShowActions] = useState(false); + + const onButtonClick = useCallback(() => { + setShowActions(!showActions); + }, [showActions]); + + const onClosePopover = useCallback((cb?: () => void) => { + setShowActions(false); + if (cb) { + cb(); + } + }, []); + + return ( + + + + } + id="settingsPopover" + isOpen={showActions} + closePopover={onClosePopover} + > + + {propertyActions.map(action => ( + + onClosePopover(action.onClick)} + /> + + ))} + + + + + ); +}); + +PropertyActions.displayName = 'PropertyActions'; From dc3a58f416c3a6858a069dee036db0be8318daaa Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Sat, 8 Feb 2020 15:41:01 -0700 Subject: [PATCH 49/67] improve update case hook --- .../components/notes/add_note/new_note.tsx | 2 - .../siem/public/containers/case/api.ts | 8 +- .../siem/public/containers/case/constants.ts | 16 +-- .../siem/public/containers/case/types.ts | 19 +++ .../public/containers/case/use_get_case.tsx | 9 +- .../containers/case/use_update_case.tsx | 81 ++++++++---- .../siem/public/pages/case/case_details.tsx | 6 - .../pages/case/components/case_view/index.tsx | 119 ++++++++++-------- .../pages/case/components/case_view/schema.ts | 24 ---- .../pages/case/components/create/index.tsx | 1 + .../description_md_editor/index.tsx | 29 ++++- .../components/property_actions/index.tsx | 5 +- .../case/server/saved_object_mappings.ts | 95 -------------- 13 files changed, 191 insertions(+), 223 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts delete mode 100644 x-pack/plugins/case/server/saved_object_mappings.ts diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx index 5a3439d53dd891..15e58f3efd21ec 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx @@ -32,8 +32,6 @@ const TextArea = styled(EuiTextArea)<{ height: number }>` TextArea.displayName = 'TextArea'; -TextArea.displayName = 'TextArea'; - /** An input for entering a new note */ export const NewNote = React.memo<{ noteInputHeight: number; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 08f4bb4ec2c919..d0cfeb2712a5e5 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -11,6 +11,8 @@ import { NewCase, NewCaseFormatted, SortFieldCase, + UpdateCase, + UpdateCaseSavedObject, } from './types'; import { Direction } from '../../graphql/types'; import { throwIfNotOk } from '../../hooks/api/api'; @@ -86,10 +88,10 @@ export const createCase = async (newCase: NewCase): Promise => return response.json(); }; -export const updateCase = async ( +export const updateCaseProperty = async ( caseId: string, - updatedCase: NewCaseFormatted -): Promise => { + updatedCase: UpdateCase +): Promise => { const response = await fetch(`${chrome.getBasePath()}${CASES_URL}/${caseId}`, { method: 'POST', credentials: 'same-origin', diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index f49ba5f255a090..51da94f70e935b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CASES_URL = `/api/cases`; +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 5; +export const FETCH_FAILURE = 'FETCH_FAILURE'; export const FETCH_INIT = 'FETCH_INIT'; export const FETCH_SUCCESS = 'FETCH_SUCCESS'; -export const FETCH_FAILURE = 'FETCH_FAILURE'; -export const UPDATE_PAGINATION = 'UPDATE_PAGINATION'; -export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; export const POST_NEW_CASE = 'POST_NEW_CASE'; -export const UPDATE_CASE = 'UPDATE_CASE'; export const REFRESH_CASE = 'REFRESH_CASE'; -export const DEFAULT_TABLE_ACTIVE_PAGE = 1; -export const DEFAULT_TABLE_LIMIT = 5; - -export const CASES_URL = `/api/cases`; +export const UPDATE_CASE = 'UPDATE_CASE'; +export const UPDATE_CASE_PROPERTY = 'UPDATE_CASE_PROPERTY'; +export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; +export const UPDATE_PAGINATION = 'UPDATE_PAGINATION'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 49c5f8eddce516..f8aac3db142355 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -20,6 +20,16 @@ export interface NewCase extends FormData { export interface NewCaseFormatted extends NewCase { state: string; + updated_at: number; +} + +export interface UpdateCase { + case_type?: string; + description?: string; + state?: string; + tags?: string[]; + title?: string; + updated_at?: number; } interface Case { @@ -40,6 +50,14 @@ export interface CasesSavedObjects { total: number; } +export interface UpdateCaseSavedObject { + attributes: UpdateCase; + id: string; + type: string; + updated_at: string; + version: string; +} + export interface CaseSavedObject { attributes: CaseResult; id: string; @@ -47,6 +65,7 @@ export interface CaseSavedObject { updated_at: string; version: string; } + export interface CaseResult { case_type: string; created_at: number; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index fb94d52f841b14..05c06311bafb71 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -83,12 +83,13 @@ const initialRefreshData: NewCaseFormatted = { state: '', tags: [], title: '', + updated_at: 0, }; -export const useGetCase = ( - initialCaseId: string -): [CaseState, Dispatch>] => { + +export type RefreshCase = Dispatch>; +export const useGetCase = (initialCaseId: string): [CaseState, RefreshCase] => { const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: false, + isLoading: true, isError: false, data: initialData, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index f565fe59ddbb73..887bbadce53472 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -4,23 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useEffect, useReducer } from 'react'; import { useStateToaster } from '../../components/toasters'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, UPDATE_CASE } from './constants'; -import { CaseSavedObject, NewCaseFormatted } from './types'; -import { updateCase } from './api'; +import { + FETCH_FAILURE, + FETCH_INIT, + FETCH_SUCCESS, + UPDATE_CASE, + UPDATE_CASE_PROPERTY, +} from './constants'; +import { CaseSavedObject, FlattenedCaseSavedObject, UpdateCase } from './types'; +import { updateCaseProperty } from './api'; + +type UpdateKey = keyof UpdateCase; interface NewCaseState { - data: NewCaseFormatted; + data: FlattenedCaseSavedObject; newCase?: CaseSavedObject; isLoading: boolean; isError: boolean; + updateKey?: UpdateKey | null; +} + +interface UpdateByKey { + updateKey: UpdateKey; + updateValue: UpdateCase[UpdateKey]; } + interface Action { type: string; - payload?: NewCaseFormatted | CaseSavedObject; + payload?: UpdateCase | CaseSavedObject | UpdateByKey; } const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { @@ -31,22 +46,39 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => ...state, isLoading: true, isError: false, + updateKey: null, }; case UPDATE_CASE: - getTypedPayload = (a: Action['payload']) => a as NewCaseFormatted; + getTypedPayload = (a: Action['payload']) => a as FlattenedCaseSavedObject; return { ...state, isLoading: false, isError: false, data: getTypedPayload(action.payload), }; + case UPDATE_CASE_PROPERTY: + getTypedPayload = (a: Action['payload']) => a as UpdateByKey; + const { updateKey, updateValue } = getTypedPayload(action.payload); + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + [updateKey]: updateValue, + }, + updateKey, + }; case FETCH_SUCCESS: - getTypedPayload = (a: Action['payload']) => a as CaseSavedObject; + getTypedPayload = (a: Action['payload']) => a as UpdateCase; return { ...state, isLoading: false, isError: false, - newCase: getTypedPayload(action.payload), + data: { + ...state.data, + ...getTypedPayload(action.payload), + }, }; case FETCH_FAILURE: return { @@ -61,36 +93,37 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => export const useUpdateCase = ( caseId: string, - initialData: NewCaseFormatted -): [Dispatch>] => { + initialData: FlattenedCaseSavedObject +): [{ data: FlattenedCaseSavedObject }, (updates: UpdateByKey) => void] => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); - const [formData, setFormData] = useState(initialData); const [, dispatchToaster] = useStateToaster(); - useEffect(() => { - dispatch({ type: UPDATE_CASE, payload: formData }); - }, [formData]); + const dispatchUpdateCaseProperty = ({ updateKey, updateValue }: UpdateByKey) => { + dispatch({ + type: UPDATE_CASE_PROPERTY, + payload: { updateKey, updateValue }, + }); + }; useEffect(() => { - const fetchData = async () => { + const fetchData = async (updateKey: keyof UpdateCase) => { dispatch({ type: FETCH_INIT }); try { - const dataWithoutIsNew = state.data; - delete dataWithoutIsNew.isNew; - const response = await updateCase(caseId, dataWithoutIsNew); - dispatch({ type: FETCH_SUCCESS, payload: response }); + const response = await updateCaseProperty(caseId, { [updateKey]: state.data[updateKey] }); + dispatch({ type: FETCH_SUCCESS, payload: response.attributes }); } catch (error) { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); dispatch({ type: FETCH_FAILURE }); } }; - if (state.data.isNew) { - fetchData(); + if (state.updateKey) { + fetchData(state.updateKey); } - }, [state.data.isNew]); - return [setFormData]; + }, [state.updateKey]); + + return [{ data: state.data }, dispatchUpdateCaseProperty]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index 651fc1596bd980..75ae6cf25f879c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; -import { WrapperPage } from '../../components/wrapper_page'; import { CaseView } from './components/case_view'; import { SpyRoute } from '../../utils/route/spy_routes'; @@ -17,11 +15,7 @@ interface Props { export const CaseDetailsPage = React.memo(({ caseId }: Props) => ( <> - {/* */} - {/* */} - {/* */} - {/* */} )); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 49cc8fa8450dc5..501dbcc113ecf8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -6,16 +6,16 @@ import React, { useCallback, useEffect, useState } from 'react'; import { + EuiBadge, EuiButton, EuiButtonEmpty, + EuiButtonToggle, + EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - EuiDescriptionList, - EuiButtonToggle, - EuiBadge, } from '@elastic/eui'; import styled, { css } from 'styled-components'; @@ -24,13 +24,11 @@ import { HeaderPage } from '../../../../components/header_page_new'; import { WrapperPage } from '../../../../components/wrapper_page'; import * as i18n from './translations'; import { getCaseUrl } from '../../../../components/link_to'; -import { useGetCase } from '../../../../containers/case/use_get_case'; +import { useGetCase, RefreshCase } from '../../../../containers/case/use_get_case'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; -import { Form, useForm } from '../shared_imports'; -import { schema } from './schema'; import { DescriptionMarkdown } from '../description_md_editor'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; -import { NewCaseFormatted } from '../../../../containers/case/types'; +import { FlattenedCaseSavedObject } from '../../../../containers/case/types'; import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { TagList } from '../tag_list'; @@ -66,33 +64,42 @@ const BackgroundWrapper = styled.div` `} `; -export const CaseView = React.memo(({ caseId }: Props) => { - const [{ data, isLoading, isError }, refreshCase] = useGetCase(caseId); - if (isError) { - return null; - } - const [setFormData] = useUpdateCase(caseId, data); - const { form } = useForm({ - defaultValue: data, - options: { stripEmptyFields: false }, - schema, - }); - const [isEdit, setIsEdit] = useState(false); - const [isCaseOpen, setIsCaseOpen] = useState(data.state.toLowerCase() === 'open'); +interface CasesProps { + caseId: string; + initialData: FlattenedCaseSavedObject; + isLoading: boolean; + refreshCase: RefreshCase; +} + +export const Cases = React.memo(({ caseId, initialData, isLoading, refreshCase }) => { + const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); + const [isEditDescription, setIsEditDescription] = useState(false); + const [isCaseOpen, setIsCaseOpen] = useState(data.state === 'open'); + const [description, setDescription] = useState(data.description); - const onSubmit = useCallback(async () => { - const { isValid, data: newData } = await form.submit(); - if (isValid) { - setFormData({ ...newData, isNew: true } as NewCaseFormatted); - refreshCase(newData as NewCaseFormatted); - setIsEdit(false); + const onUpdateDescription = useCallback(async () => { + if (description.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'description', + updateValue: description, + }); + setIsEditDescription(false); } - }, [form]); + }, [dispatchUpdateCaseProperty]); + useEffect(() => { + const caseState = isCaseOpen ? 'open' : 'closed'; + if (data.state !== caseState) { + dispatchUpdateCaseProperty({ + updateKey: 'state', + updateValue: caseState, + }); + } + }, [isCaseOpen]); const propertyActions = [ { iconType: 'documentEdit', label: 'Edit description', - onClick: () => setIsEdit(true), + onClick: () => setIsEditDescription(true), }, { iconType: 'securitySignalResolved', @@ -137,23 +144,30 @@ export const CaseView = React.memo(({ caseId }: Props) => { ), - children: isEdit ? ( + children: isEditDescription ? ( <> setFormData({ ...data, description })} + onChange={updatedDescription => setDescription(updatedDescription)} /> - + {i18n.SUBMIT} - setIsEdit(false)}>{i18n.CANCEL} + setIsEditDescription(false)}> + {i18n.CANCEL} + @@ -173,18 +187,7 @@ export const CaseView = React.memo(({ caseId }: Props) => { children:

      {'alright alright alright'}

      , }, ]; - - useEffect(() => { - setIsCaseOpen(data.state.toLowerCase() === 'open'); - }, [data.state]); - - return isLoading ? ( - - - - - - ) : ( + return ( <> { - {isEdit ? ( -
      - - - ) : ( - - )} +
      @@ -254,4 +251,24 @@ export const CaseView = React.memo(({ caseId }: Props) => { ); }); +export const CaseView = React.memo(({ caseId }: Props) => { + const [{ data, isLoading, isError }, refreshCase] = useGetCase(caseId); + if (isError) { + return null; + } + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + ); +}); + CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts deleted file mode 100644 index 6e02b7f31dfdbb..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema as initialCaseSchema } from '../create/schema'; - -import { FIELD_TYPES, fieldValidators, FormSchema } from '../shared_imports'; -import * as i18n from '../../translations'; -const { emptyField } = fieldValidators; - -export const schema: FormSchema = { - ...initialCaseSchema, - state: { - type: FIELD_TYPES.SUPER_SELECT, - label: i18n.STATE, - validations: [ - { - validator: emptyField(i18n.STATE_REQUIRED), - }, - ], - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index d575e735639949..13df59a1561b2e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -69,6 +69,7 @@ export const Create = React.memo(() => { /> setFormData({ ...data, description })} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx index ecfa0f9a70c7ad..44062a5a1d5897 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx @@ -4,14 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem, EuiPanel, EuiTabbedContent } from '@elastic/eui'; +import { EuiFlexItem, EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; import React, { useState } from 'react'; import styled from 'styled-components'; import { Markdown } from '../../../../components/markdown'; import * as i18n from '../../translations'; -import { CommonUseField } from '../create'; import { MarkdownHint } from '../../../../components/markdown/markdown_hint'; +import { CommonUseField } from '../create'; + +const TextArea = styled(EuiTextArea)<{ height: number }>` + min-height: ${({ height }) => `${height}px`}; + width: 100%; +`; + +TextArea.displayName = 'TextArea'; const DescriptionContainer = styled.div` margin-top: 15px; @@ -36,14 +43,15 @@ export const DescriptionMarkdown = React.memo<{ descriptionInputHeight: number; initialDescription: string; isLoading: boolean; + formHook?: boolean; onChange: (description: string) => void; -}>(({ initialDescription, isLoading, descriptionInputHeight, onChange }) => { +}>(({ initialDescription, isLoading, descriptionInputHeight, onChange, formHook = false }) => { const [description, setDescription] = useState(initialDescription); const tabs = [ { id: 'description', name: i18n.DESCRIPTION, - content: ( + content: formHook ? ( { @@ -57,6 +65,19 @@ export const DescriptionMarkdown = React.memo<{ spellcheck: false, }} /> + ) : ( +