[SecuritySolution][Timeline] Refactor timeline HTTP API (#200633)

## Summary

The timeline API endpoints are currently implemented from a mix of HTTP
and GraphQL practices. Since GraphQL has been removed for a long time
now, we should make sure the endpoints conform to HTTP best practices.
This will allow us to simplify the API- and client-logic. Further,
third-parties accessing these APIs will have an easier time integrating.

### Usage of HTTP status codes

Depending on the error, the API endpoints currently return a `200` with
`{ code: 404, message: '(...)' }` or an actual HTTP error response with
e.g. `403` and the message in the body. The practice of returning 200s
with embedded error codes comes from GraphQL, where error codes are
always embedded.

Example of a current HTTP response of a failed timeline request:

```
HTTP status: 200
HTTP body:
{
  "error_code": 409,
  "messsage": "there was a conflict"
}
```

Going forward, all endpoints should return proper error codes and embed
the error messages in the response body.
```
HTTP status: 409
HTTP body:
{
  "messsage": "there was a conflict"
}
```

### Removal of `{}` responses

Some timeline endpoints might return empty objects in case they were not
able to resolve/retrieve some SOs. The empty object implies a `404`
response. This creates complications on the client that now have to
provide extra logic for how to interpret empty objects.

Example of a current request of one of the endpoints that allows empty
responses.
```
HTTP status: 200
{}
```
The absence of an object, on some of the listed endpoints, indicates a
404 or the top-level or embedded saved object.

Going forward, the endpoints should not return empty objects and instead
return the proper HTTP error code (e.g. `404`) or a success code.

```
HTTP status: 404
```

### No more nested bodies

Another relic of the GraphQL time is the nesting of request bodies like
this:

```
HTTP status: 200
HTTP body:
{
  "data": {
    "persistTimeline": {
      (actual timeline object)
    }
  }
}
```

Combined with sometimes returning empty objects and potentially
returning a status code in the body, makes it overly complicated for
clients to reason about the response.

Going forward, the actual object(s) should be returned as a top-level
JSON object, omitting `data.persistX`.
```
HTTP status: 200
HTTP body:
{
  (actual timeline object)
}
```

### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jan Monschke 2024-11-22 07:54:45 +01:00 committed by GitHub
parent 50236c4677
commit 82108f134e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 1131 additions and 3522 deletions

View file

@ -31617,13 +31617,6 @@ paths:
required: true
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
description: Indicates the note was successfully deleted.
summary: Delete a note
tags:
@ -31685,9 +31678,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
oneOf:
- $ref: '#/components/schemas/Security_Timeline_API_GetNotesResult'
- type: object
$ref: '#/components/schemas/Security_Timeline_API_GetNotesResult'
description: Indicates the requested notes were returned.
summary: Get notes
tags:
@ -31731,17 +31722,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
persistNote:
$ref: '#/components/schemas/Security_Timeline_API_ResponseNote'
required:
- persistNote
required:
- data
$ref: '#/components/schemas/Security_Timeline_API_ResponseNote'
description: Indicates the note was successfully created.
summary: Add or update a note
tags:
@ -32094,17 +32075,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
persistPinnedEventOnTimeline:
$ref: '#/components/schemas/Security_Timeline_API_PersistPinnedEventResponse'
required:
- persistPinnedEventOnTimeline
required:
- data
$ref: '#/components/schemas/Security_Timeline_API_PersistPinnedEventResponse'
description: Indicates the event was successfully pinned to the Timeline.
summary: Pin an event
tags:
@ -33715,20 +33686,6 @@ paths:
required: true
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
deleteTimeline:
type: boolean
required:
- deleteTimeline
required:
- data
description: Indicates the Timeline was successfully deleted.
summary: Delete Timelines or Timeline templates
tags:
@ -33753,20 +33710,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
oneOf:
- type: object
properties:
data:
type: object
properties:
getOneTimeline:
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
required:
- getOneTimeline
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
description: Indicates that the (template) Timeline was found and returned.
summary: Get Timeline or Timeline template details
tags:
@ -34077,17 +34021,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
persistFavorite:
$ref: '#/components/schemas/Security_Timeline_API_FavoriteTimelineResponse'
required:
- persistFavorite
required:
- data
$ref: '#/components/schemas/Security_Timeline_API_FavoriteTimelineResponse'
description: Indicates the favorite status was successfully updated.
'403':
content:
@ -34244,15 +34178,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
oneOf:
- type: object
properties:
data:
$ref: '#/components/schemas/Security_Timeline_API_ResolvedTimeline'
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/Security_Timeline_API_ResolvedTimeline'
description: The (template) Timeline has been found
'400':
description: The request is missing parameters
@ -47290,16 +47216,10 @@ components:
Security_Timeline_API_FavoriteTimelineResponse:
type: object
properties:
code:
nullable: true
type: number
favorite:
items:
$ref: '#/components/schemas/Security_Timeline_API_FavoriteTimelineResult'
type: array
message:
nullable: true
type: string
savedObjectId:
type: string
templateTimelineId:
@ -47468,28 +47388,15 @@ components:
- version
Security_Timeline_API_PersistPinnedEventResponse:
oneOf:
- allOf:
- $ref: '#/components/schemas/Security_Timeline_API_PinnedEvent'
- $ref: '#/components/schemas/Security_Timeline_API_PinnedEventBaseResponseBody'
- nullable: true
type: object
Security_Timeline_API_PersistTimelineResponse:
type: object
properties:
data:
type: object
- $ref: '#/components/schemas/Security_Timeline_API_PinnedEvent'
- type: object
properties:
persistTimeline:
type: object
properties:
timeline:
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
required:
- timeline
unpinned:
type: boolean
required:
- persistTimeline
required:
- data
- unpinned
Security_Timeline_API_PersistTimelineResponse:
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
Security_Timeline_API_PinnedEvent:
allOf:
- $ref: '#/components/schemas/Security_Timeline_API_BarePinnedEvent'
@ -47502,15 +47409,6 @@ components:
required:
- pinnedEventId
- version
Security_Timeline_API_PinnedEventBaseResponseBody:
type: object
properties:
code:
type: number
message:
type: string
required:
- code
Security_Timeline_API_QueryMatchResult:
type: object
properties:
@ -47551,15 +47449,9 @@ components:
Security_Timeline_API_ResponseNote:
type: object
properties:
code:
type: number
message:
type: string
note:
$ref: '#/components/schemas/Security_Timeline_API_Note'
required:
- code
- message
- note
Security_Timeline_API_RowRendererId:
enum:

View file

@ -34361,13 +34361,6 @@ paths:
required: true
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
description: Indicates the note was successfully deleted.
summary: Delete a note
tags:
@ -34428,9 +34421,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
oneOf:
- $ref: '#/components/schemas/Security_Timeline_API_GetNotesResult'
- type: object
$ref: '#/components/schemas/Security_Timeline_API_GetNotesResult'
description: Indicates the requested notes were returned.
summary: Get notes
tags:
@ -34473,17 +34464,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
persistNote:
$ref: '#/components/schemas/Security_Timeline_API_ResponseNote'
required:
- persistNote
required:
- data
$ref: '#/components/schemas/Security_Timeline_API_ResponseNote'
description: Indicates the note was successfully created.
summary: Add or update a note
tags:
@ -34821,17 +34802,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
persistPinnedEventOnTimeline:
$ref: '#/components/schemas/Security_Timeline_API_PersistPinnedEventResponse'
required:
- persistPinnedEventOnTimeline
required:
- data
$ref: '#/components/schemas/Security_Timeline_API_PersistPinnedEventResponse'
description: Indicates the event was successfully pinned to the Timeline.
summary: Pin an event
tags:
@ -37370,20 +37341,6 @@ paths:
required: true
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
deleteTimeline:
type: boolean
required:
- deleteTimeline
required:
- data
description: Indicates the Timeline was successfully deleted.
summary: Delete Timelines or Timeline templates
tags:
@ -37407,20 +37364,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
oneOf:
- type: object
properties:
data:
type: object
properties:
getOneTimeline:
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
required:
- getOneTimeline
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
description: Indicates that the (template) Timeline was found and returned.
summary: Get Timeline or Timeline template details
tags:
@ -37724,17 +37668,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
data:
type: object
properties:
persistFavorite:
$ref: '#/components/schemas/Security_Timeline_API_FavoriteTimelineResponse'
required:
- persistFavorite
required:
- data
$ref: '#/components/schemas/Security_Timeline_API_FavoriteTimelineResponse'
description: Indicates the favorite status was successfully updated.
'403':
content:
@ -37888,15 +37822,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
oneOf:
- type: object
properties:
data:
$ref: '#/components/schemas/Security_Timeline_API_ResolvedTimeline'
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/Security_Timeline_API_ResolvedTimeline'
description: The (template) Timeline has been found
'400':
description: The request is missing parameters
@ -55013,16 +54939,10 @@ components:
Security_Timeline_API_FavoriteTimelineResponse:
type: object
properties:
code:
nullable: true
type: number
favorite:
items:
$ref: '#/components/schemas/Security_Timeline_API_FavoriteTimelineResult'
type: array
message:
nullable: true
type: string
savedObjectId:
type: string
templateTimelineId:
@ -55191,28 +55111,15 @@ components:
- version
Security_Timeline_API_PersistPinnedEventResponse:
oneOf:
- allOf:
- $ref: '#/components/schemas/Security_Timeline_API_PinnedEvent'
- $ref: '#/components/schemas/Security_Timeline_API_PinnedEventBaseResponseBody'
- nullable: true
type: object
Security_Timeline_API_PersistTimelineResponse:
type: object
properties:
data:
type: object
- $ref: '#/components/schemas/Security_Timeline_API_PinnedEvent'
- type: object
properties:
persistTimeline:
type: object
properties:
timeline:
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
required:
- timeline
unpinned:
type: boolean
required:
- persistTimeline
required:
- data
- unpinned
Security_Timeline_API_PersistTimelineResponse:
$ref: '#/components/schemas/Security_Timeline_API_TimelineResponse'
Security_Timeline_API_PinnedEvent:
allOf:
- $ref: '#/components/schemas/Security_Timeline_API_BarePinnedEvent'
@ -55225,15 +55132,6 @@ components:
required:
- pinnedEventId
- version
Security_Timeline_API_PinnedEventBaseResponseBody:
type: object
properties:
code:
type: number
message:
type: string
required:
- code
Security_Timeline_API_QueryMatchResult:
type: object
properties:
@ -55274,15 +55172,9 @@ components:
Security_Timeline_API_ResponseNote:
type: object
properties:
code:
type: number
message:
type: string
note:
$ref: '#/components/schemas/Security_Timeline_API_Note'
required:
- code
- message
- note
Security_Timeline_API_RowRendererId:
enum:

View file

@ -299,14 +299,8 @@ import type {
CreateTimelinesRequestBodyInput,
CreateTimelinesResponse,
} from './timeline/create_timelines/create_timelines_route.gen';
import type {
DeleteNoteRequestBodyInput,
DeleteNoteResponse,
} from './timeline/delete_note/delete_note_route.gen';
import type {
DeleteTimelinesRequestBodyInput,
DeleteTimelinesResponse,
} from './timeline/delete_timelines/delete_timelines_route.gen';
import type { DeleteNoteRequestBodyInput } from './timeline/delete_note/delete_note_route.gen';
import type { DeleteTimelinesRequestBodyInput } from './timeline/delete_timelines/delete_timelines_route.gen';
import type {
ExportTimelinesRequestQueryInput,
ExportTimelinesRequestBodyInput,
@ -768,7 +762,7 @@ If a record already exists for the specified entity, that record is overwritten
async deleteNote(props: DeleteNoteProps) {
this.log.info(`${new Date().toISOString()} Calling API DeleteNote`);
return this.kbnClient
.request<DeleteNoteResponse>({
.request({
path: '/api/note',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
@ -801,7 +795,7 @@ If a record already exists for the specified entity, that record is overwritten
async deleteTimelines(props: DeleteTimelinesProps) {
this.log.info(`${new Date().toISOString()} Calling API DeleteTimelines`);
return this.kbnClient
.request<DeleteTimelinesResponse>({
.request({
path: '/api/timeline',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',

View file

@ -30,8 +30,3 @@ export const DeleteNoteRequestBody = z.union([
.nullable(),
]);
export type DeleteNoteRequestBodyInput = z.input<typeof DeleteNoteRequestBody>;
export type DeleteNoteResponse = z.infer<typeof DeleteNoteResponse>;
export const DeleteNoteResponse = z.object({
data: z.object({}).optional(),
});

View file

@ -37,10 +37,3 @@ paths:
responses:
'200':
description: Indicates the note was successfully deleted.
content:
application/json:
schema:
type: object
properties:
data:
type: object

View file

@ -25,10 +25,3 @@ export const DeleteTimelinesRequestBody = z.object({
searchIds: z.array(z.string()).optional(),
});
export type DeleteTimelinesRequestBodyInput = z.input<typeof DeleteTimelinesRequestBody>;
export type DeleteTimelinesResponse = z.infer<typeof DeleteTimelinesResponse>;
export const DeleteTimelinesResponse = z.object({
data: z.object({
deleteTimeline: z.boolean(),
}),
});

View file

@ -36,15 +36,3 @@ paths:
responses:
'200':
description: Indicates the Timeline was successfully deleted.
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
type: object
required: [deleteTimeline]
properties:
deleteTimeline:
type: boolean

View file

@ -60,4 +60,4 @@ export const GetNotesRequestQuery = z.object({
export type GetNotesRequestQueryInput = z.input<typeof GetNotesRequestQuery>;
export type GetNotesResponse = z.infer<typeof GetNotesResponse>;
export const GetNotesResponse = z.union([GetNotesResult, z.object({})]);
export const GetNotesResponse = GetNotesResult;

View file

@ -66,9 +66,7 @@ paths:
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/GetNotesResult'
- type: object
$ref: '#/components/schemas/GetNotesResult'
components:
schemas:

View file

@ -32,11 +32,4 @@ export const GetTimelineRequestQuery = z.object({
export type GetTimelineRequestQueryInput = z.input<typeof GetTimelineRequestQuery>;
export type GetTimelineResponse = z.infer<typeof GetTimelineResponse>;
export const GetTimelineResponse = z.union([
z.object({
data: z.object({
getOneTimeline: TimelineResponse,
}),
}),
z.object({}).strict(),
]);
export const GetTimelineResponse = TimelineResponse;

View file

@ -32,15 +32,4 @@ paths:
content:
application/json:
schema:
oneOf:
- type: object
required: [data]
properties:
data:
type: object
required: [getOneTimeline]
properties:
getOneTimeline:
$ref: '../model/components.schema.yaml#/components/schemas/TimelineResponse'
- type: object
additionalProperties: false
$ref: '../model/components.schema.yaml#/components/schemas/TimelineResponse'

View file

@ -309,8 +309,6 @@ export type FavoriteTimelineResponse = z.infer<typeof FavoriteTimelineResponse>;
export const FavoriteTimelineResponse = z.object({
savedObjectId: z.string(),
version: z.string(),
code: z.number().nullable().optional(),
message: z.string().nullable().optional(),
templateTimelineId: z.string().nullable().optional(),
templateTimelineVersion: z.number().nullable().optional(),
timelineType: TimelineType.optional(),
@ -318,13 +316,7 @@ export const FavoriteTimelineResponse = z.object({
});
export type PersistTimelineResponse = z.infer<typeof PersistTimelineResponse>;
export const PersistTimelineResponse = z.object({
data: z.object({
persistTimeline: z.object({
timeline: TimelineResponse,
}),
}),
});
export const PersistTimelineResponse = TimelineResponse;
export type BareNoteWithoutExternalRefs = z.infer<typeof BareNoteWithoutExternalRefs>;
export const BareNoteWithoutExternalRefs = z.object({

View file

@ -225,12 +225,6 @@ components:
type: string
version:
type: string
code:
type: number
nullable: true
message:
type: string
nullable: true
templateTimelineId:
type: string
nullable: true
@ -244,19 +238,7 @@ components:
items:
$ref: '#/components/schemas/FavoriteTimelineResult'
PersistTimelineResponse:
type: object
required: [data]
properties:
data:
type: object
required: [persistTimeline]
properties:
persistTimeline:
type: object
required: [timeline]
properties:
timeline:
$ref: '#/components/schemas/TimelineResponse'
$ref: '#/components/schemas/TimelineResponse'
ColumnHeaderResult:
type: object
properties:

View file

@ -28,8 +28,4 @@ export const PersistFavoriteRouteRequestBody = z.object({
export type PersistFavoriteRouteRequestBodyInput = z.input<typeof PersistFavoriteRouteRequestBody>;
export type PersistFavoriteRouteResponse = z.infer<typeof PersistFavoriteRouteResponse>;
export const PersistFavoriteRouteResponse = z.object({
data: z.object({
persistFavorite: FavoriteTimelineResponse,
}),
});
export const PersistFavoriteRouteResponse = FavoriteTimelineResponse;

View file

@ -39,15 +39,8 @@ paths:
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
type: object
required: [persistFavorite]
properties:
persistFavorite:
$ref: '../model/components.schema.yaml#/components/schemas/FavoriteTimelineResponse'
$ref: '../model/components.schema.yaml#/components/schemas/FavoriteTimelineResponse'
'403':
description: Indicates the user does not have the required permissions to persist the favorite status.
content:

View file

@ -20,8 +20,6 @@ import { BareNote, Note } from '../model/components.gen';
export type ResponseNote = z.infer<typeof ResponseNote>;
export const ResponseNote = z.object({
code: z.number(),
message: z.string(),
note: Note,
});
@ -38,8 +36,4 @@ export const PersistNoteRouteRequestBody = z.object({
export type PersistNoteRouteRequestBodyInput = z.input<typeof PersistNoteRouteRequestBody>;
export type PersistNoteRouteResponse = z.infer<typeof PersistNoteRouteResponse>;
export const PersistNoteRouteResponse = z.object({
data: z.object({
persistNote: ResponseNote,
}),
});
export const PersistNoteRouteResponse = ResponseNote;

View file

@ -50,24 +50,12 @@ paths:
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
type: object
required: [persistNote]
properties:
persistNote:
$ref: '#/components/schemas/ResponseNote'
$ref: '#/components/schemas/ResponseNote'
components:
schemas:
ResponseNote:
type: object
required: [code, message, note]
required: [note]
properties:
code:
type: number
message:
type: string
note:
$ref: '../model/components.schema.yaml#/components/schemas/Note'

View file

@ -18,16 +18,12 @@ import { z } from '@kbn/zod';
import { PinnedEvent } from '../model/components.gen';
export type PinnedEventBaseResponseBody = z.infer<typeof PinnedEventBaseResponseBody>;
export const PinnedEventBaseResponseBody = z.object({
code: z.number(),
message: z.string().optional(),
});
export type PersistPinnedEventResponse = z.infer<typeof PersistPinnedEventResponse>;
export const PersistPinnedEventResponse = z.union([
PinnedEvent.merge(PinnedEventBaseResponseBody),
z.object({}).nullable(),
PinnedEvent,
z.object({
unpinned: z.boolean(),
}),
]);
export type PersistPinnedEventRouteRequestBody = z.infer<typeof PersistPinnedEventRouteRequestBody>;
@ -41,8 +37,4 @@ export type PersistPinnedEventRouteRequestBodyInput = z.input<
>;
export type PersistPinnedEventRouteResponse = z.infer<typeof PersistPinnedEventRouteResponse>;
export const PersistPinnedEventRouteResponse = z.object({
data: z.object({
persistPinnedEventOnTimeline: PersistPinnedEventResponse,
}),
});
export const PersistPinnedEventRouteResponse = PersistPinnedEventResponse;

View file

@ -37,30 +37,15 @@ paths:
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
type: object
required: [persistPinnedEventOnTimeline]
properties:
persistPinnedEventOnTimeline:
$ref: '#/components/schemas/PersistPinnedEventResponse'
$ref: '#/components/schemas/PersistPinnedEventResponse'
components:
schemas:
PersistPinnedEventResponse:
oneOf:
- allOf:
- $ref: '../model/components.schema.yaml#/components/schemas/PinnedEvent'
- $ref: '#/components/schemas/PinnedEventBaseResponseBody'
- $ref: '../model/components.schema.yaml#/components/schemas/PinnedEvent'
- type: object
nullable: true
PinnedEventBaseResponseBody:
type: object
required: [code]
properties:
code:
type: number
message:
type: string
required: [unpinned]
properties:
unpinned:
type: boolean

View file

@ -32,9 +32,4 @@ export const ResolveTimelineRequestQuery = z.object({
export type ResolveTimelineRequestQueryInput = z.input<typeof ResolveTimelineRequestQuery>;
export type ResolveTimelineResponse = z.infer<typeof ResolveTimelineResponse>;
export const ResolveTimelineResponse = z.union([
z.object({
data: ResolvedTimeline,
}),
z.object({}).strict(),
]);
export const ResolveTimelineResponse = ResolvedTimeline;

View file

@ -28,14 +28,7 @@ paths:
content:
application/json:
schema:
oneOf:
- type: object
required: [data]
properties:
data:
$ref: '../model/components.schema.yaml#/components/schemas/ResolvedTimeline'
- type: object
additionalProperties: false
$ref: '../model/components.schema.yaml#/components/schemas/ResolvedTimeline'
'400':
description: The request is missing parameters

View file

@ -5,17 +5,14 @@
* 2.0.
*/
export {
DeleteTimelinesRequestBody,
DeleteTimelinesResponse,
} from './delete_timelines/delete_timelines_route.gen';
export { DeleteTimelinesRequestBody } from './delete_timelines/delete_timelines_route.gen';
export {
PersistNoteRouteRequestBody,
PersistNoteRouteResponse,
ResponseNote,
} from './persist_note/persist_note_route.gen';
export { DeleteNoteRequestBody, DeleteNoteResponse } from './delete_note/delete_note_route.gen';
export { DeleteNoteRequestBody } from './delete_note/delete_note_route.gen';
export {
CleanDraftTimelinesResponse,

View file

@ -43,13 +43,6 @@ paths:
required: true
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data:
type: object
description: Indicates the note was successfully deleted.
summary: Delete a note
tags:
@ -111,9 +104,7 @@ paths:
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/GetNotesResult'
- type: object
$ref: '#/components/schemas/GetNotesResult'
description: Indicates the requested notes were returned.
summary: Get notes
tags:
@ -157,17 +148,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
persistNote:
$ref: '#/components/schemas/ResponseNote'
required:
- persistNote
required:
- data
$ref: '#/components/schemas/ResponseNote'
description: Indicates the note was successfully created.
summary: Add or update a note
tags:
@ -200,17 +181,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
persistPinnedEventOnTimeline:
$ref: '#/components/schemas/PersistPinnedEventResponse'
required:
- persistPinnedEventOnTimeline
required:
- data
$ref: '#/components/schemas/PersistPinnedEventResponse'
description: Indicates the event was successfully pinned to the Timeline.
summary: Pin an event
tags:
@ -243,20 +214,6 @@ paths:
required: true
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
deleteTimeline:
type: boolean
required:
- deleteTimeline
required:
- data
description: Indicates the Timeline was successfully deleted.
summary: Delete Timelines or Timeline templates
tags:
@ -281,20 +238,7 @@ paths:
content:
application/json:
schema:
oneOf:
- type: object
properties:
data:
type: object
properties:
getOneTimeline:
$ref: '#/components/schemas/TimelineResponse'
required:
- getOneTimeline
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/TimelineResponse'
description: Indicates that the (template) Timeline was found and returned.
summary: Get Timeline or Timeline template details
tags:
@ -636,17 +580,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
persistFavorite:
$ref: '#/components/schemas/FavoriteTimelineResponse'
required:
- persistFavorite
required:
- data
$ref: '#/components/schemas/FavoriteTimelineResponse'
description: Indicates the favorite status was successfully updated.
'403':
content:
@ -811,15 +745,7 @@ paths:
content:
application/json:
schema:
oneOf:
- type: object
properties:
data:
$ref: '#/components/schemas/ResolvedTimeline'
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/ResolvedTimeline'
description: The (template) Timeline has been found
'400':
description: The request is missing parameters
@ -1089,16 +1015,10 @@ components:
FavoriteTimelineResponse:
type: object
properties:
code:
nullable: true
type: number
favorite:
items:
$ref: '#/components/schemas/FavoriteTimelineResult'
type: array
message:
nullable: true
type: string
savedObjectId:
type: string
templateTimelineId:
@ -1267,28 +1187,15 @@ components:
- version
PersistPinnedEventResponse:
oneOf:
- allOf:
- $ref: '#/components/schemas/PinnedEvent'
- $ref: '#/components/schemas/PinnedEventBaseResponseBody'
- nullable: true
type: object
PersistTimelineResponse:
type: object
properties:
data:
type: object
- $ref: '#/components/schemas/PinnedEvent'
- type: object
properties:
persistTimeline:
type: object
properties:
timeline:
$ref: '#/components/schemas/TimelineResponse'
required:
- timeline
unpinned:
type: boolean
required:
- persistTimeline
required:
- data
- unpinned
PersistTimelineResponse:
$ref: '#/components/schemas/TimelineResponse'
PinnedEvent:
allOf:
- $ref: '#/components/schemas/BarePinnedEvent'
@ -1301,15 +1208,6 @@ components:
required:
- pinnedEventId
- version
PinnedEventBaseResponseBody:
type: object
properties:
code:
type: number
message:
type: string
required:
- code
QueryMatchResult:
type: object
properties:
@ -1350,15 +1248,9 @@ components:
ResponseNote:
type: object
properties:
code:
type: number
message:
type: string
note:
$ref: '#/components/schemas/Note'
required:
- code
- message
- note
RowRendererId:
enum:

View file

@ -43,13 +43,6 @@ paths:
required: true
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data:
type: object
description: Indicates the note was successfully deleted.
summary: Delete a note
tags:
@ -111,9 +104,7 @@ paths:
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/GetNotesResult'
- type: object
$ref: '#/components/schemas/GetNotesResult'
description: Indicates the requested notes were returned.
summary: Get notes
tags:
@ -157,17 +148,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
persistNote:
$ref: '#/components/schemas/ResponseNote'
required:
- persistNote
required:
- data
$ref: '#/components/schemas/ResponseNote'
description: Indicates the note was successfully created.
summary: Add or update a note
tags:
@ -200,17 +181,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
persistPinnedEventOnTimeline:
$ref: '#/components/schemas/PersistPinnedEventResponse'
required:
- persistPinnedEventOnTimeline
required:
- data
$ref: '#/components/schemas/PersistPinnedEventResponse'
description: Indicates the event was successfully pinned to the Timeline.
summary: Pin an event
tags:
@ -243,20 +214,6 @@ paths:
required: true
responses:
'200':
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
deleteTimeline:
type: boolean
required:
- deleteTimeline
required:
- data
description: Indicates the Timeline was successfully deleted.
summary: Delete Timelines or Timeline templates
tags:
@ -281,20 +238,7 @@ paths:
content:
application/json:
schema:
oneOf:
- type: object
properties:
data:
type: object
properties:
getOneTimeline:
$ref: '#/components/schemas/TimelineResponse'
required:
- getOneTimeline
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/TimelineResponse'
description: Indicates that the (template) Timeline was found and returned.
summary: Get Timeline or Timeline template details
tags:
@ -636,17 +580,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
persistFavorite:
$ref: '#/components/schemas/FavoriteTimelineResponse'
required:
- persistFavorite
required:
- data
$ref: '#/components/schemas/FavoriteTimelineResponse'
description: Indicates the favorite status was successfully updated.
'403':
content:
@ -811,15 +745,7 @@ paths:
content:
application/json:
schema:
oneOf:
- type: object
properties:
data:
$ref: '#/components/schemas/ResolvedTimeline'
required:
- data
- additionalProperties: false
type: object
$ref: '#/components/schemas/ResolvedTimeline'
description: The (template) Timeline has been found
'400':
description: The request is missing parameters
@ -1089,16 +1015,10 @@ components:
FavoriteTimelineResponse:
type: object
properties:
code:
nullable: true
type: number
favorite:
items:
$ref: '#/components/schemas/FavoriteTimelineResult'
type: array
message:
nullable: true
type: string
savedObjectId:
type: string
templateTimelineId:
@ -1267,28 +1187,15 @@ components:
- version
PersistPinnedEventResponse:
oneOf:
- allOf:
- $ref: '#/components/schemas/PinnedEvent'
- $ref: '#/components/schemas/PinnedEventBaseResponseBody'
- nullable: true
type: object
PersistTimelineResponse:
type: object
properties:
data:
type: object
- $ref: '#/components/schemas/PinnedEvent'
- type: object
properties:
persistTimeline:
type: object
properties:
timeline:
$ref: '#/components/schemas/TimelineResponse'
required:
- timeline
unpinned:
type: boolean
required:
- persistTimeline
required:
- data
- unpinned
PersistTimelineResponse:
$ref: '#/components/schemas/TimelineResponse'
PinnedEvent:
allOf:
- $ref: '#/components/schemas/BarePinnedEvent'
@ -1301,15 +1208,6 @@ components:
required:
- pinnedEventId
- version
PinnedEventBaseResponseBody:
type: object
properties:
code:
type: number
message:
type: string
required:
- code
QueryMatchResult:
type: object
properties:
@ -1350,15 +1248,9 @@ components:
ResponseNote:
type: object
properties:
code:
type: number
message:
type: string
note:
$ref: '#/components/schemas/Note'
required:
- code
- message
- note
RowRendererId:
enum:

View file

@ -2015,15 +2015,6 @@ export const mockGetOneTimelineResult: TimelineResponse = {
version: '1',
};
export const mockTimelineResult = {
data: {
getOneTimeline: mockGetOneTimelineResult,
},
loading: false,
networkStatus: 7,
stale: false,
};
export const defaultTimelineProps: CreateTimelineProps = {
from: '2018-11-05T18:58:25.937Z',
timeline: {

View file

@ -18,6 +18,7 @@ import { mockHistory, Router } from '../../../../common/mock/router';
import { render, act, fireEvent } from '@testing-library/react';
import { resolveTimeline } from '../../../../timelines/containers/api';
import { mockTimeline } from '../../../../../server/lib/timeline/__mocks__/create_timelines';
import type { ResolveTimelineResponse } from '../../../../../common/api/timeline';
jest.mock('../../../../timelines/containers/api');
jest.mock('../../../../common/lib/kibana', () => {
@ -49,6 +50,11 @@ jest.mock('../../../../timelines/containers/all', () => {
};
});
const resolvedTimeline: ResolveTimelineResponse = {
timeline: { ...mockTimeline, savedObjectId: '1', version: 'abc' },
outcome: 'exactMatch',
};
describe('QueryBarDefineRule', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -59,11 +65,7 @@ describe('QueryBarDefineRule', () => {
totalCount: mockOpenTimelineQueryResults.totalCount,
refetch: jest.fn(),
});
(resolveTimeline as jest.Mock).mockResolvedValue({
data: {
timeline: { mockTimeline },
},
});
(resolveTimeline as jest.Mock).mockResolvedValue(resolvedTimeline);
});
it('renders correctly', () => {

View file

@ -25,7 +25,6 @@ import {
getThresholdDetectionAlertAADMock,
mockEcsDataWithAlert,
mockTimelineDetails,
mockTimelineResult,
mockAADEcsDataWithAlert,
mockGetOneTimelineResult,
mockTimelineData,
@ -283,7 +282,7 @@ describe('alert actions', () => {
search: jest.fn().mockImplementation(() => of({ data: mockTimelineDetails })),
};
(getTimelineTemplate as jest.Mock).mockResolvedValue(mockTimelineResult);
(getTimelineTemplate as jest.Mock).mockResolvedValue(mockGetOneTimelineResult);
clock = sinon.useFakeTimers(unix);
});
@ -442,11 +441,13 @@ describe('alert actions', () => {
test('it invokes createTimeline with kqlQuery.filterQuery.kuery.kind as "kuery" if not specified in returned timeline template', async () => {
const mockTimelineResultModified = {
...mockTimelineResult,
kqlQuery: {
filterQuery: {
kuery: {
expression: [''],
body: {
...mockGetOneTimelineResult,
kqlQuery: {
filterQuery: {
kuery: {
expression: [''],
},
},
},
},
@ -460,7 +461,6 @@ describe('alert actions', () => {
getExceptionFilter: mockGetExceptionFilter,
});
const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0];
expect(mockGetExceptionFilter).not.toHaveBeenCalled();
expect(createTimeline).toHaveBeenCalledTimes(1);
expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery');

View file

@ -7,7 +7,7 @@
/* eslint-disable complexity */
import { getOr, isEmpty } from 'lodash/fp';
import { isEmpty } from 'lodash/fp';
import moment from 'moment';
import dateMath from '@kbn/datemath';
@ -51,7 +51,6 @@ import {
isThresholdRule,
} from '../../../../common/detection_engine/utils';
import { TimelineId } from '../../../../common/types/timeline';
import type { TimelineResponse } from '../../../../common/api/timeline';
import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../common/api/timeline';
import type {
SendAlertToTimelineActionProps,
@ -67,10 +66,7 @@ import type {
} from '../../../../common/search_strategy/timeline';
import { TimelineEventsQueries } from '../../../../common/search_strategy/timeline';
import { timelineDefaults } from '../../../timelines/store/defaults';
import {
omitTypenameInTimeline,
formatTimelineResponseToModel,
} from '../../../timelines/components/open_timeline/helpers';
import { formatTimelineResponseToModel } from '../../../timelines/components/open_timeline/helpers';
import { convertKueryToElasticSearchQuery } from '../../../common/lib/kuery';
import { getField, getFieldKey } from '../../../helpers';
import {
@ -980,15 +976,9 @@ export const sendAlertToTimelineAction = async ({
)
),
]);
const resultingTimeline: TimelineResponse = getOr(
{},
'data.getOneTimeline',
responseTimeline
);
const eventData: TimelineEventsDetailsItem[] = eventDataResp.data ?? [];
if (!isEmpty(resultingTimeline)) {
const timelineTemplate = omitTypenameInTimeline(resultingTimeline);
if (!isEmpty(responseTimeline)) {
const timelineTemplate = responseTimeline;
const { timeline, notes } = formatTimelineResponseToModel(
timelineTemplate,
true,

View file

@ -112,114 +112,110 @@ const mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction')
});
const mockTimelineTemplateResponse = {
data: {
getOneTimeline: {
savedObjectId: '15bc8185-06ef-4956-b7e7-be8e289b13c2',
version: 'WzIzMzUsMl0=',
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'date',
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
},
],
dataProviders: [
{
and: [],
enabled: true,
id: 'some-random-id',
name: 'host.name',
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'host.name',
value: '{host.name}',
operator: ':',
},
type: 'template',
},
],
dataViewId: 'security-solution-default',
description: '',
eqlOptions: {
eventCategoryField: 'event.category',
tiebreakerField: '',
timestampField: '@timestamp',
query: '',
size: 100,
savedObjectId: '15bc8185-06ef-4956-b7e7-be8e289b13c2',
version: 'WzIzMzUsMl0=',
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
type: 'date',
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
},
],
dataProviders: [
{
and: [],
enabled: true,
id: 'some-random-id',
name: 'host.name',
excluded: false,
kqlQuery: '',
queryMatch: {
field: 'host.name',
value: '{host.name}',
operator: ':',
},
eventType: 'all',
excludedRowRendererIds: [
'alert',
'alerts',
'auditd',
'auditd_file',
'library',
'netflow',
'plain',
'registry',
'suricata',
'system',
'system_dns',
'system_endgame_process',
'system_file',
'system_fim',
'system_security_event',
'system_socket',
'threat_match',
'zeek',
],
favorite: [],
filters: [],
indexNames: ['.alerts-security.alerts-default', 'auditbeat-*', 'filebeat-*', 'packetbeat-*'],
kqlMode: 'filter',
kqlQuery: {
filterQuery: {
kuery: {
kind: 'kuery',
expression: '*',
},
serializedQuery: '{"query_string":{"query":"*"}}',
},
type: 'template',
},
],
dataViewId: 'security-solution-default',
description: '',
eqlOptions: {
eventCategoryField: 'event.category',
tiebreakerField: '',
timestampField: '@timestamp',
query: '',
size: 100,
},
eventType: 'all',
excludedRowRendererIds: [
'alert',
'alerts',
'auditd',
'auditd_file',
'library',
'netflow',
'plain',
'registry',
'suricata',
'system',
'system_dns',
'system_endgame_process',
'system_file',
'system_fim',
'system_security_event',
'system_socket',
'threat_match',
'zeek',
],
favorite: [],
filters: [],
indexNames: ['.alerts-security.alerts-default', 'auditbeat-*', 'filebeat-*', 'packetbeat-*'],
kqlMode: 'filter',
kqlQuery: {
filterQuery: {
kuery: {
kind: 'kuery',
expression: '*',
},
title: 'Named Template',
templateTimelineId: 'c755cda6-8a65-4ec2-b6ff-35a5356de8b9',
templateTimelineVersion: 1,
dateRange: {
start: '2024-08-13T22:00:00.000Z',
end: '2024-08-14T21:59:59.999Z',
},
savedQueryId: null,
created: 1723625359467,
createdBy: 'elastic',
updated: 1723625359988,
updatedBy: 'elastic',
timelineType: 'template',
status: 'active',
sort: [
{
columnId: '@timestamp',
columnType: 'date',
sortDirection: 'desc',
esTypes: ['date'],
},
],
savedSearchId: null,
eventIdToNoteIds: [],
noteIds: [],
notes: [],
pinnedEventIds: [],
pinnedEventsSaveObject: [],
serializedQuery: '{"query_string":{"query":"*"}}',
},
},
title: 'Named Template',
templateTimelineId: 'c755cda6-8a65-4ec2-b6ff-35a5356de8b9',
templateTimelineVersion: 1,
dateRange: {
start: '2024-08-13T22:00:00.000Z',
end: '2024-08-14T21:59:59.999Z',
},
savedQueryId: null,
created: 1723625359467,
createdBy: 'elastic',
updated: 1723625359988,
updatedBy: 'elastic',
timelineType: 'template',
status: 'active',
sort: [
{
columnId: '@timestamp',
columnType: 'date',
sortDirection: 'desc',
esTypes: ['date'],
},
],
savedSearchId: null,
eventIdToNoteIds: [],
noteIds: [],
notes: [],
pinnedEventIds: [],
pinnedEventsSaveObject: [],
};
const props = {

View file

@ -16,6 +16,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { mockTimeline } from '../../../../../server/lib/timeline/__mocks__/create_timelines';
import type { TimelineModel } from '../../../..';
import type { ResolveTimelineResponse } from '../../../../../common/api/timeline';
jest.mock('../../../../common/hooks/use_experimental_features');
jest.mock('../../../../common/utils/global_query_string/helpers');
@ -45,53 +46,52 @@ jest.mock('react-redux', () => {
const timelineId = 'eb2781c0-1df5-11eb-8589-2f13958b79f7';
const selectedTimeline = {
data: {
timeline: {
...mockTimeline,
id: timelineId,
savedObjectId: timelineId,
indexNames: ['awesome-*'],
dataViewId: 'custom-data-view-id',
kqlQuery: {
filterQuery: {
serializedQuery:
'{"bool":{"filter":[{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"user.name"}}],"minimum_should_match":1}}]}}',
kuery: {
expression: 'host.name:* AND user.name:*',
kind: 'kuery',
},
const selectedTimeline: ResolveTimelineResponse = {
outcome: 'exactMatch',
timeline: {
...mockTimeline,
savedObjectId: timelineId,
version: 'wedwed',
indexNames: ['awesome-*'],
dataViewId: 'custom-data-view-id',
kqlQuery: {
filterQuery: {
serializedQuery:
'{"bool":{"filter":[{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"user.name"}}],"minimum_should_match":1}}]}}',
kuery: {
expression: 'host.name:* AND user.name:*',
kind: 'kuery',
},
},
dataProviders: [
{
excluded: false,
and: [],
kqlQuery: '',
name: 'Stephs-MBP.lan',
queryMatch: {
field: 'host.name',
value: 'Stephs-MBP.lan',
operator: ':',
},
id: 'draggable-badge-default-draggable-process_stopped-timeline-1-NH9UwoMB2HTqQ3G4wUFM-host_name-Stephs-MBP_lan',
enabled: true,
},
{
excluded: false,
and: [],
kqlQuery: '',
name: '--lang=en-US',
queryMatch: {
field: 'process.args',
value: '--lang=en-US',
operator: ':',
},
id: 'draggable-badge-default-draggable-process_started-timeline-1-args-5---lang=en-US-MH9TwoMB2HTqQ3G4_UH--process_args---lang=en-US',
enabled: true,
},
],
},
dataProviders: [
{
excluded: false,
and: [],
kqlQuery: '',
name: 'Stephs-MBP.lan',
queryMatch: {
field: 'host.name',
value: 'Stephs-MBP.lan',
operator: ':',
},
id: 'draggable-badge-default-draggable-process_stopped-timeline-1-NH9UwoMB2HTqQ3G4wUFM-host_name-Stephs-MBP_lan',
enabled: true,
},
{
excluded: false,
and: [],
kqlQuery: '',
name: '--lang=en-US',
queryMatch: {
field: 'process.args',
value: '--lang=en-US',
operator: ':',
},
id: 'draggable-badge-default-draggable-process_started-timeline-1-args-5---lang=en-US-MH9TwoMB2HTqQ3G4_UH--process_args---lang=en-US',
enabled: true,
},
],
},
};
@ -157,8 +157,8 @@ describe('useRuleFromTimeline', () => {
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW',
payload: {
id: 'timeline',
selectedDataViewId: selectedTimeline.data.timeline.dataViewId,
selectedPatterns: selectedTimeline.data.timeline.indexNames,
selectedDataViewId: selectedTimeline.timeline.dataViewId,
selectedPatterns: selectedTimeline.timeline.indexNames,
},
});
});
@ -220,16 +220,15 @@ describe('useRuleFromTimeline', () => {
query: 'find it EQL',
size: 100,
};
const eqlTimeline = {
data: {
timeline: {
...mockTimeline,
id: timelineId,
savedObjectId: timelineId,
indexNames: ['awesome-*'],
dataViewId: 'custom-data-view-id',
eqlOptions,
},
const eqlTimeline: ResolveTimelineResponse = {
outcome: 'exactMatch',
timeline: {
...mockTimeline,
version: '123',
savedObjectId: timelineId,
indexNames: ['awesome-*'],
dataViewId: 'custom-data-view-id',
eqlOptions,
},
};
(resolveTimeline as jest.Mock).mockResolvedValue(eqlTimeline);
@ -256,7 +255,7 @@ describe('useRuleFromTimeline', () => {
const { result } = renderHook(() => useRuleFromTimeline(setRuleQuery));
expect(result.current.loading).toEqual(false);
await act(async () => {
result.current.onOpenTimeline(selectedTimeline.data.timeline as unknown as TimelineModel);
result.current.onOpenTimeline(selectedTimeline.timeline as unknown as TimelineModel);
});
// not loading anything as an external call to onOpenTimeline provides the timeline
@ -307,7 +306,7 @@ describe('useRuleFromTimeline', () => {
const { result } = renderHook(() => useRuleFromTimeline(setRuleQuery));
expect(result.current.loading).toEqual(false);
const tl = {
...selectedTimeline.data.timeline,
...selectedTimeline.timeline,
dataProviders: [
{
property: 'bad',

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PersistNoteRouteResponse } from '../../../common/api/timeline';
import { KibanaServices } from '../../common/lib/kibana';
import * as api from './api';
@ -22,22 +21,14 @@ describe('Notes API client', () => {
});
describe('create note', () => {
it('should throw an error when a response code other than 200 is returned', async () => {
const errorResponse: PersistNoteRouteResponse = {
data: {
persistNote: {
code: 500,
message: 'Internal server error',
note: {
timelineId: '1',
noteId: '2',
version: '3',
},
},
},
};
(KibanaServices.get as jest.Mock).mockReturnValue({
http: {
patch: jest.fn().mockReturnValue(errorResponse),
patch: jest.fn().mockRejectedValue({
body: {
status_code: 500,
message: 'Internal server error',
},
}),
},
});

View file

@ -7,7 +7,6 @@
import type {
BareNote,
DeleteNoteResponse,
GetNotesResponse,
PersistNoteRouteResponse,
} from '../../../common/api/timeline';
@ -27,11 +26,7 @@ export const createNote = async ({ note }: { note: BareNote }) => {
body: JSON.stringify({ note }),
version: '2023-10-31',
});
const noteResponse = response.data.persistNote;
if (noteResponse.code !== 200) {
throw new Error(noteResponse.message);
}
return noteResponse.note;
return response.note;
} catch (err) {
throw new Error(('message' in err && err.message) || 'Request failed');
}
@ -98,7 +93,7 @@ export const fetchNotesBySaveObjectIds = async (savedObjectIds: string[]) => {
* Deletes multiple notes
*/
export const deleteNotes = async (noteIds: string[]) => {
const response = await KibanaServices.get().http.delete<DeleteNoteResponse>(NOTE_URL, {
const response = await KibanaServices.get().http.delete(NOTE_URL, {
body: JSON.stringify({ noteIds }),
version: '2023-10-31',
});

View file

@ -5,421 +5,384 @@
* 2.0.
*/
import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../../common/api/timeline';
import {
DataProviderTypeEnum,
type ResolveTimelineResponse,
TimelineStatusEnum,
TimelineTypeEnum,
} from '../../../../../common/api/timeline';
export const mockTimeline = {
data: {
timeline: {
savedObjectId: 'eb2781c0-1df5-11eb-8589-2f13958b79f7',
columns: [
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: '@timestamp',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'message',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'event.category',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'event.action',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'host.name',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'source.ip',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'destination.ip',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'user.name',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
],
dataProviders: [],
dateRange: {
start: '2020-11-01T14:30:59.935Z',
end: '2020-11-03T14:31:11.417Z',
__typename: 'DateRangePickerResult',
export const mockTimeline: ResolveTimelineResponse = {
timeline: {
savedObjectId: 'eb2781c0-1df5-11eb-8589-2f13958b79f7',
columns: [
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: '@timestamp',
name: null,
searchable: null,
type: null,
},
description: '',
eventType: 'all',
eventIdToNoteIds: [],
excludedRowRendererIds: [],
favorite: [],
filters: [],
kqlMode: 'filter',
kqlQuery: { filterQuery: null, __typename: 'SerializedFilterQueryResult' },
indexNames: [
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
'.siem-signals-angelachuang-default',
],
notes: [],
noteIds: [],
pinnedEventIds: [],
pinnedEventsSaveObject: [],
status: TimelineStatusEnum.active,
title: 'my timeline',
timelineType: TimelineTypeEnum.default,
templateTimelineId: null,
templateTimelineVersion: null,
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
__typename: 'SortTimelineResult',
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'message',
name: null,
searchable: null,
type: null,
},
created: 1604497127973,
createdBy: 'elastic',
updated: 1604500278364,
updatedBy: 'elastic',
version: 'WzQ4NSwxXQ==',
__typename: 'TimelineResult',
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'event.category',
name: null,
searchable: null,
type: null,
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'event.action',
name: null,
searchable: null,
type: null,
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'host.name',
name: null,
searchable: null,
type: null,
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'source.ip',
name: null,
searchable: null,
type: null,
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'destination.ip',
name: null,
searchable: null,
type: null,
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'user.name',
name: null,
searchable: null,
type: null,
},
],
dataProviders: [],
dateRange: {
start: '2020-11-01T14:30:59.935Z',
end: '2020-11-03T14:31:11.417Z',
},
outcome: 'exactMatch',
description: '',
eventType: 'all',
eventIdToNoteIds: [],
excludedRowRendererIds: [],
favorite: [],
filters: [],
kqlMode: 'filter',
kqlQuery: { filterQuery: null },
indexNames: [
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
'.siem-signals-angelachuang-default',
],
notes: [],
noteIds: [],
pinnedEventIds: [],
pinnedEventsSaveObject: [],
status: TimelineStatusEnum.active,
title: 'my timeline',
timelineType: TimelineTypeEnum.default,
templateTimelineId: null,
templateTimelineVersion: null,
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
created: 1604497127973,
createdBy: 'elastic',
updated: 1604500278364,
updatedBy: 'elastic',
version: 'WzQ4NSwxXQ==',
},
loading: false,
networkStatus: 7,
stale: false,
outcome: 'exactMatch',
};
export const mockTemplate = {
data: {
timeline: {
savedObjectId: '0c70a200-1de0-11eb-885c-6fc13fca1850',
columns: [
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: '@timestamp',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'signal.rule.description',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'event.action',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'process.name',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description: 'The working directory of the process.',
example: '/home/alice',
indexes: null,
id: 'process.working_directory',
name: null,
searchable: null,
type: 'string',
__typename: 'ColumnHeaderResult',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description:
'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.',
example: '["/usr/bin/ssh","-l","user","10.0.0.16"]',
indexes: null,
id: 'process.args',
name: null,
searchable: null,
type: 'string',
__typename: 'ColumnHeaderResult',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'process.pid',
name: null,
searchable: null,
type: null,
__typename: 'ColumnHeaderResult',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description: 'Absolute path to the process executable.',
example: '/usr/bin/ssh',
indexes: null,
id: 'process.parent.executable',
name: null,
searchable: null,
type: 'string',
__typename: 'ColumnHeaderResult',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description:
'Array of process arguments.\n\nMay be filtered to protect sensitive information.',
example: '["ssh","-l","user","10.0.0.16"]',
indexes: null,
id: 'process.parent.args',
name: null,
searchable: null,
type: 'string',
__typename: 'ColumnHeaderResult',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description: 'Process id.',
example: '4242',
indexes: null,
id: 'process.parent.pid',
name: null,
searchable: null,
type: 'number',
__typename: 'ColumnHeaderResult',
},
{
aggregatable: true,
category: 'user',
columnHeaderType: 'not-filtered',
description: 'Short name or login of the user.',
example: 'albert',
indexes: null,
id: 'user.name',
name: null,
searchable: null,
type: 'string',
__typename: 'ColumnHeaderResult',
},
{
aggregatable: true,
category: 'host',
columnHeaderType: 'not-filtered',
description:
'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.',
example: null,
indexes: null,
id: 'host.name',
name: null,
searchable: null,
type: 'string',
__typename: 'ColumnHeaderResult',
},
],
dataProviders: [
{
id: 'timeline-1-8622010a-61fb-490d-b162-beac9c36a853',
name: '{process.name}',
enabled: true,
excluded: false,
kqlQuery: '',
type: 'template',
queryMatch: {
field: 'process.name',
displayField: null,
value: '{process.name}',
displayValue: null,
operator: ':',
__typename: 'QueryMatchResult',
},
and: [],
__typename: 'DataProviderResult',
},
{
id: 'timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568',
name: '{event.type}',
enabled: true,
excluded: false,
kqlQuery: '',
type: 'template',
queryMatch: {
field: 'event.type',
displayField: null,
value: '{event.type}',
displayValue: null,
operator: ':*',
__typename: 'QueryMatchResult',
},
and: [],
__typename: 'DataProviderResult',
},
],
dateRange: {
start: '2020-10-27T14:22:11.809Z',
end: '2020-11-03T14:22:11.809Z',
__typename: 'DateRangePickerResult',
export const mockTemplate: ResolveTimelineResponse = {
timeline: {
savedObjectId: '0c70a200-1de0-11eb-885c-6fc13fca1850',
columns: [
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: '@timestamp',
name: null,
searchable: null,
type: null,
},
description: '',
eventType: 'all',
eventIdToNoteIds: [],
excludedRowRendererIds: [],
favorite: [],
filters: [],
kqlMode: 'filter',
kqlQuery: {
filterQuery: {
kuery: { kind: 'kuery', expression: '', __typename: 'KueryFilterQueryResult' },
serializedQuery: '',
__typename: 'SerializedKueryQueryResult',
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'signal.rule.description',
name: null,
searchable: null,
type: null,
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'event.action',
name: null,
searchable: null,
type: null,
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'process.name',
name: null,
searchable: null,
type: null,
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description: 'The working directory of the process.',
example: '/home/alice',
indexes: null,
id: 'process.working_directory',
name: null,
searchable: null,
type: 'string',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description:
'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.',
example: '["/usr/bin/ssh","-l","user","10.0.0.16"]',
indexes: null,
id: 'process.args',
name: null,
searchable: null,
type: 'string',
},
{
aggregatable: null,
category: null,
columnHeaderType: 'not-filtered',
description: null,
example: null,
indexes: null,
id: 'process.pid',
name: null,
searchable: null,
type: null,
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description: 'Absolute path to the process executable.',
example: '/usr/bin/ssh',
indexes: null,
id: 'process.parent.executable',
name: null,
searchable: null,
type: 'string',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description:
'Array of process arguments.\n\nMay be filtered to protect sensitive information.',
example: '["ssh","-l","user","10.0.0.16"]',
indexes: null,
id: 'process.parent.args',
name: null,
searchable: null,
type: 'string',
},
{
aggregatable: true,
category: 'process',
columnHeaderType: 'not-filtered',
description: 'Process id.',
example: '4242',
indexes: null,
id: 'process.parent.pid',
name: null,
searchable: null,
type: 'number',
},
{
aggregatable: true,
category: 'user',
columnHeaderType: 'not-filtered',
description: 'Short name or login of the user.',
example: 'albert',
indexes: null,
id: 'user.name',
name: null,
searchable: null,
type: 'string',
},
{
aggregatable: true,
category: 'host',
columnHeaderType: 'not-filtered',
description:
'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.',
example: null,
indexes: null,
id: 'host.name',
name: null,
searchable: null,
type: 'string',
},
],
dataProviders: [
{
id: 'timeline-1-8622010a-61fb-490d-b162-beac9c36a853',
name: '{process.name}',
enabled: true,
excluded: false,
kqlQuery: '',
type: DataProviderTypeEnum.template,
queryMatch: {
field: 'process.name',
displayField: null,
value: '{process.name}',
displayValue: null,
operator: ':',
},
__typename: 'SerializedFilterQueryResult',
and: [],
},
indexNames: [],
notes: [],
noteIds: [],
pinnedEventIds: [],
pinnedEventsSaveObject: [],
status: TimelineStatusEnum.immutable,
title: 'Generic Process Timeline',
timelineType: 'template',
templateTimelineId: 'cd55e52b-7bce-4887-88e2-f1ece4c75447',
templateTimelineVersion: 1,
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
__typename: 'SortTimelineResult',
{
id: 'timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568',
name: '{event.type}',
enabled: true,
excluded: false,
kqlQuery: '',
type: DataProviderTypeEnum.template,
queryMatch: {
field: 'event.type',
displayField: null,
value: '{event.type}',
displayValue: null,
operator: ':*',
},
and: [],
},
created: 1604413368243,
createdBy: 'angela',
updated: 1604413368243,
updatedBy: 'angela',
version: 'WzQwMywxXQ==',
__typename: 'TimelineResult',
],
dateRange: {
start: '2020-10-27T14:22:11.809Z',
end: '2020-11-03T14:22:11.809Z',
},
outcome: 'exactMatch',
description: '',
eventType: 'all',
eventIdToNoteIds: [],
excludedRowRendererIds: [],
favorite: [],
filters: [],
kqlMode: 'filter',
kqlQuery: {
filterQuery: {
kuery: { kind: 'kuery', expression: '' },
serializedQuery: '',
},
},
indexNames: [],
notes: [],
noteIds: [],
pinnedEventIds: [],
pinnedEventsSaveObject: [],
status: TimelineStatusEnum.immutable,
title: 'Generic Process Timeline',
timelineType: TimelineTypeEnum.template,
templateTimelineId: 'cd55e52b-7bce-4887-88e2-f1ece4c75447',
templateTimelineVersion: 1,
savedQueryId: null,
sort: {
columnId: '@timestamp',
columnType: 'number',
sortDirection: 'desc',
},
created: 1604413368243,
createdBy: 'angela',
updated: 1604413368243,
updatedBy: 'angela',
version: 'WzQwMywxXQ==',
},
loading: false,
networkStatus: 7,
stale: false,
outcome: 'exactMatch',
};

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { cloneDeep, getOr, omit } from 'lodash/fp';
import { cloneDeep, omit } from 'lodash/fp';
import { renderHook } from '@testing-library/react-hooks';
import { waitFor } from '@testing-library/react';
import { mockTimelineResults, mockGetOneTimelineResult } from '../../../common/mock';
import { mockTimelineResults } from '../../../common/mock';
import { timelineDefaults } from '../../store/defaults';
import type { QueryTimelineById } from './helpers';
import {
@ -17,7 +17,6 @@ import {
getNotesCount,
getPinnedEventCount,
isUntitled,
omitTypenameInTimeline,
useQueryTimelineById,
formatTimelineResponseToModel,
} from './helpers';
@ -655,7 +654,7 @@ describe('helpers', () => {
test('Do not override daterange if TimelineStatus is active', () => {
const { timeline } = formatTimelineResponseToModel(
omitTypenameInTimeline(getOr({}, 'data.timeline', selectedTimeline)),
selectedTimeline.timeline,
args.duplicate,
args.timelineType
);
@ -667,7 +666,7 @@ describe('helpers', () => {
describe('update a timeline', () => {
const selectedTimeline = { ...mockSelectedTimeline };
const untitledTimeline = { ...mockSelectedTimeline, title: '' };
const untitledTimeline = { timeline: { ...mockSelectedTimeline.timeline, title: '' } };
const onOpenTimeline = jest.fn();
const args: QueryTimelineById = {
duplicate: false,
@ -684,7 +683,6 @@ describe('helpers', () => {
afterEach(() => {
jest.clearAllMocks();
});
test('should get timeline by Id with correct statuses', async () => {
renderHook(async () => {
const queryTimelineById = useQueryTimelineById();
@ -693,7 +691,7 @@ describe('helpers', () => {
// expect(resolveTimeline).toHaveBeenCalled();
const { timeline } = formatTimelineResponseToModel(
omitTypenameInTimeline(getOr({}, 'data.timeline', selectedTimeline)),
selectedTimeline.timeline,
args.duplicate,
args.timelineType
);
@ -751,7 +749,7 @@ describe('helpers', () => {
});
test('should update timeline correctly when timeline is already saved and onOpenTimeline is not provided', async () => {
(resolveTimeline as jest.Mock).mockResolvedValue(mockSelectedTimeline);
(resolveTimeline as jest.Mock).mockResolvedValue(selectedTimeline);
renderHook(async () => {
const queryTimelineById = useQueryTimelineById();
queryTimelineById(args);
@ -762,7 +760,7 @@ describe('helpers', () => {
1,
expect.objectContaining({
timeline: expect.objectContaining({
columns: mockSelectedTimeline.data.timeline.columns.map((col) => ({
columns: selectedTimeline.timeline.columns!.map((col) => ({
columnHeaderType: col.columnHeaderType,
id: col.id,
initialWidth: defaultUdtHeaders.find((defaultCol) => col.id === defaultCol.id)
@ -784,7 +782,7 @@ describe('helpers', () => {
waitFor(() => {
expect(onOpenTimeline).toHaveBeenCalledWith(
expect.objectContaining({
columns: mockSelectedTimeline.data.timeline.columns.map((col) => ({
columns: mockSelectedTimeline.timeline.columns!.map((col) => ({
columnHeaderType: col.columnHeaderType,
id: col.id,
initialWidth: defaultUdtHeaders.find((defaultCol) => col.id === defaultCol.id)
@ -827,7 +825,7 @@ describe('helpers', () => {
test('override daterange if TimelineStatus is immutable', () => {
const { timeline } = formatTimelineResponseToModel(
omitTypenameInTimeline(getOr({}, 'data.timeline', template)),
template.timeline,
args.duplicate,
args.timelineType
);
@ -841,26 +839,4 @@ describe('helpers', () => {
});
});
});
describe('omitTypenameInTimeline', () => {
test('should not modify the passed in timeline if no __typename exists', () => {
const result = omitTypenameInTimeline(mockGetOneTimelineResult);
expect(result).toEqual(mockGetOneTimelineResult);
});
test('should return timeline with __typename removed when it exists', () => {
const mockTimeline = {
...mockGetOneTimelineResult,
__typename: 'something, something',
};
const result = omitTypenameInTimeline(mockTimeline);
const expectedTimeline = {
...mockTimeline,
__typename: undefined,
};
expect(result).toEqual(expectedTimeline);
});
});
});

View file

@ -13,7 +13,6 @@ import { useDiscoverInTimelineContext } from '../../../common/components/discove
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
import type {
TimelineResponse,
ResolvedTimeline,
ColumnHeaderResult,
FilterTimelineResult,
DataProviderResult,
@ -73,12 +72,6 @@ export const getNotesCount = ({ eventIdToNoteIds, noteIds }: OpenTimelineResult)
export const isUntitled = ({ title }: OpenTimelineResult): boolean =>
title == null || title.trim().length === 0;
const omitTypename = (key: string, value: keyof TimelineModel) =>
key === '__typename' ? undefined : value;
export const omitTypenameInTimeline = (timeline: TimelineResponse): TimelineResponse =>
JSON.parse(JSON.stringify(timeline), omitTypename);
const parseString = (params: string) => {
try {
return JSON.parse(params);
@ -348,13 +341,10 @@ export const useQueryTimelineById = () => {
} else {
return Promise.resolve(resolveTimeline(timelineId))
.then((result) => {
const data: ResolvedTimeline | null = getOr(null, 'data', result);
if (!data) return;
const timelineToOpen = omitTypenameInTimeline(data.timeline);
if (!result) return;
const { timeline, notes } = formatTimelineResponseToModel(
timelineToOpen,
result.timeline,
duplicate,
timelineType
);
@ -372,9 +362,9 @@ export const useQueryTimelineById = () => {
id: TimelineId.active,
notes,
resolveTimelineConfig: {
outcome: data.outcome,
alias_target_id: data.alias_target_id,
alias_purpose: data.alias_purpose,
outcome: result.outcome,
alias_target_id: result.alias_target_id,
alias_purpose: result.alias_purpose,
},
timeline: {
...timeline,

View file

@ -88,17 +88,9 @@ const timelineData = {
savedSearchId: null,
};
const mockPatchTimelineResponse = {
data: {
persistTimeline: {
code: 200,
message: 'success',
timeline: {
...timelineData,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzM0NSwxXQ==',
},
},
},
...timelineData,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzM0NSwxXQ==',
};
describe('persistTimeline', () => {
describe('create draft timeline', () => {
@ -108,15 +100,9 @@ describe('persistTimeline', () => {
status: TimelineStatusEnum.draft,
};
const mockDraftResponse = {
data: {
persistTimeline: {
timeline: {
...initialDraftTimeline,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzMzMiwxXQ==',
},
},
},
...initialDraftTimeline,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzMzMiwxXQ==',
};
const version = null;
@ -161,14 +147,13 @@ describe('persistTimeline', () => {
test("it should update timeline from clean draft timeline's response", () => {
expect(JSON.parse(patchMock.mock.calls[0][1].body)).toEqual({
timelineId: mockDraftResponse.data.persistTimeline.timeline.savedObjectId,
timelineId: mockDraftResponse.savedObjectId,
timeline: {
...initialDraftTimeline,
templateTimelineId: mockDraftResponse.data.persistTimeline.timeline.templateTimelineId,
templateTimelineVersion:
mockDraftResponse.data.persistTimeline.timeline.templateTimelineVersion,
templateTimelineId: mockDraftResponse.templateTimelineId,
templateTimelineVersion: mockDraftResponse.templateTimelineVersion,
},
version: mockDraftResponse.data.persistTimeline.timeline.version ?? '',
version: mockDraftResponse.version ?? '',
});
});
});
@ -211,13 +196,8 @@ describe('persistTimeline', () => {
version,
});
expect(persist).toEqual({
data: {
persistTimeline: {
code: 403,
message: 'you do not have the permission',
timeline: { ...initialDraftTimeline, savedObjectId: '', version: '' },
},
},
statusCode: 403,
message: 'you do not have the permission',
});
});
});
@ -226,15 +206,9 @@ describe('persistTimeline', () => {
const timelineId = null;
const importTimeline = timelineData;
const mockPostTimelineResponse = {
data: {
persistTimeline: {
timeline: {
...timelineData,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzMzMiwxXQ==',
},
},
},
...timelineData,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzMzMiwxXQ==',
};
const version = null;
@ -273,17 +247,11 @@ describe('persistTimeline', () => {
const timelineId = '9d5693e0-a42a-11ea-b8f4-c5434162742a';
const inputTimeline = timelineData;
const mockPatchTimelineResponseNew = {
data: {
persistTimeline: {
timeline: {
...mockPatchTimelineResponse.data.persistTimeline.timeline,
version: 'WzMzMiwxXQ==',
description: 'x',
created: 1591092702804,
updated: 1591092705206,
},
},
},
...mockPatchTimelineResponse,
version: 'WzMzMiwxXQ==',
description: 'x',
created: 1591092702804,
updated: 1591092705206,
};
const version = 'initial version';
@ -454,15 +422,9 @@ describe('cleanDraftTimeline', () => {
describe('copyTimeline', () => {
const mockPostTimelineResponse = {
data: {
persistTimeline: {
timeline: {
...timelineData,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzMzMiwxXQ==',
},
},
},
...timelineData,
savedObjectId: '9d5693e0-a42a-11ea-b8f4-c5434162742a',
version: 'WzMzMiwxXQ==',
};
const saveSavedSearchMock = jest.fn();

View file

@ -67,26 +67,30 @@ const createToasterPlainError = (message: string) => new ToasterError([message])
const parseOrThrow = parseOrThrowErrorFactory(createToasterPlainError);
const decodeTimelineResponse = (respTimeline?: PersistTimelineResponse | TimelineErrorResponse) =>
parseOrThrow(PersistTimelineResponse)(respTimeline);
const decodeTimelineResponse = (
respTimeline?: PersistTimelineResponse | TimelineErrorResponse
): PersistTimelineResponse => parseOrThrow(PersistTimelineResponse)(respTimeline);
const decodeSingleTimelineResponse = (respTimeline?: GetTimelineResponse) =>
const decodeSingleTimelineResponse = (respTimeline?: GetTimelineResponse): GetTimelineResponse =>
parseOrThrow(GetTimelineResponse)(respTimeline);
const decodeResolvedSingleTimelineResponse = (respTimeline?: ResolveTimelineResponse) =>
parseOrThrow(ResolveTimelineResponse)(respTimeline);
const decodeResolvedSingleTimelineResponse = (
respTimeline?: ResolveTimelineResponse
): ResolveTimelineResponse => parseOrThrow(ResolveTimelineResponse)(respTimeline);
const decodeGetTimelinesResponse = (respTimeline: GetTimelinesResponse) =>
const decodeGetTimelinesResponse = (respTimeline: GetTimelinesResponse): GetTimelinesResponse =>
parseOrThrow(GetTimelinesResponse)(respTimeline);
const decodeTimelineErrorResponse = (respTimeline?: TimelineErrorResponse) =>
const decodeTimelineErrorResponse = (respTimeline?: TimelineErrorResponse): TimelineErrorResponse =>
parseOrThrow(TimelineErrorResponse)(respTimeline);
const decodePrepackedTimelineResponse = (respTimeline?: ImportTimelineResult) =>
parseOrThrow(ImportTimelineResult)(respTimeline);
const decodePrepackedTimelineResponse = (
respTimeline?: ImportTimelineResult
): ImportTimelineResult => parseOrThrow(ImportTimelineResult)(respTimeline);
const decodeResponseFavoriteTimeline = (respTimeline?: PersistFavoriteRouteResponse) =>
parseOrThrow(PersistFavoriteRouteResponse)(respTimeline);
const decodeResponseFavoriteTimeline = (
respTimeline?: PersistFavoriteRouteResponse
): PersistFavoriteRouteResponse => parseOrThrow(PersistFavoriteRouteResponse)(respTimeline);
const postTimeline = async ({
timeline,
@ -219,22 +223,19 @@ export const persistTimeline = async ({
const templateTimelineInfo =
timeline.timelineType === TimelineTypeEnum.template
? {
templateTimelineId:
draftTimeline.data.persistTimeline.timeline.templateTimelineId ??
timeline.templateTimelineId,
templateTimelineId: draftTimeline.templateTimelineId ?? timeline.templateTimelineId,
templateTimelineVersion:
draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ??
timeline.templateTimelineVersion,
draftTimeline.templateTimelineVersion ?? timeline.templateTimelineVersion,
}
: {};
return patchTimeline({
timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId,
timelineId: draftTimeline.savedObjectId,
timeline: {
...timeline,
...templateTimelineInfo,
},
version: draftTimeline.data.persistTimeline.timeline.version ?? '',
version: draftTimeline.version ?? '',
savedSearch,
});
}
@ -250,19 +251,10 @@ export const persistTimeline = async ({
savedSearch,
});
} catch (err) {
if (err.status_code === 403 || err.body.status_code === 403) {
if (err.status_code === 403 || err.body?.status_code === 403) {
return Promise.resolve({
data: {
persistTimeline: {
code: 403,
message: err.message || err.body.message,
timeline: {
...timeline,
savedObjectId: '',
version: '',
},
},
},
statusCode: 403,
message: err.message || err.body.message,
});
}
return Promise.resolve(err);

View file

@ -39,16 +39,8 @@ describe('Timeline middleware helpers', () => {
it('should return a draft timeline with a savedObjectId when an unsaved timeline is passed', async () => {
const mockSavedObjectId = 'mockSavedObjectId';
(persistTimeline as jest.Mock).mockResolvedValue({
data: {
persistTimeline: {
code: 200,
message: 'success',
timeline: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
savedObjectId: mockSavedObjectId,
},
},
},
...mockGlobalState.timeline.timelineById[TimelineId.test],
savedObjectId: mockSavedObjectId,
});
const returnedTimeline = await ensureTimelineIsSaved({

View file

@ -6,6 +6,7 @@
*/
import type { MiddlewareAPI, Dispatch, AnyAction } from 'redux';
import type { IHttpFetchError } from '@kbn/core/public';
import type { State } from '../../../common/store/types';
import { ALL_TIMELINE_QUERY_ID } from '../../containers/all';
import type { inputsModel } from '../../../common/store/inputs';
@ -62,3 +63,17 @@ export async function ensureTimelineIsSaved({
// Make sure we're returning the most updated version of the timeline
return selectTimelineById(store.getState(), localTimelineId);
}
export function isHttpFetchError(
error: unknown
): error is IHttpFetchError<{ status_code: number }> {
return (
error !== null &&
typeof error === 'object' &&
'body' in error &&
error.body !== null &&
typeof error.body === 'object' &&
`status_code` in error.body &&
typeof error.body.status_code === 'number'
);
}

View file

@ -35,7 +35,14 @@ jest.mock('../actions', () => {
};
});
jest.mock('../../containers/api');
jest.mock('./helpers');
jest.mock('./helpers', () => {
const actual = jest.requireActual('./helpers');
return {
...actual,
refreshTimelines: jest.fn(),
};
});
const startTimelineSavingMock = startTimelineSaving as unknown as jest.Mock;
const endTimelineSavingMock = endTimelineSaving as unknown as jest.Mock;
@ -53,14 +60,9 @@ describe('Timeline favorite middleware', () => {
it('should persist a timeline favorite when a favorite action is dispatched', async () => {
(persistFavorite as jest.Mock).mockResolvedValue({
data: {
persistFavorite: {
code: 200,
favorite: [{}],
savedObjectId: newSavedObjectId,
version: newVersion,
},
},
favorite: [{}],
savedObjectId: newSavedObjectId,
version: newVersion,
});
expect(selectTimelineById(store.getState(), TimelineId.test).isFavorite).toEqual(false);
await store.dispatch(updateIsFavorite({ id: TimelineId.test, isFavorite: true }));
@ -88,14 +90,9 @@ describe('Timeline favorite middleware', () => {
})
);
(persistFavorite as jest.Mock).mockResolvedValue({
data: {
persistFavorite: {
code: 200,
favorite: [],
savedObjectId: newSavedObjectId,
version: newVersion,
},
},
favorite: [],
savedObjectId: newSavedObjectId,
version: newVersion,
});
expect(selectTimelineById(store.getState(), TimelineId.test).isFavorite).toEqual(true);
await store.dispatch(updateIsFavorite({ id: TimelineId.test, isFavorite: false }));
@ -113,12 +110,8 @@ describe('Timeline favorite middleware', () => {
});
it('should show an error message when the call is unauthorized', async () => {
(persistFavorite as jest.Mock).mockResolvedValue({
data: {
persistFavorite: {
code: 403,
},
},
(persistFavorite as jest.Mock).mockRejectedValue({
body: { status_code: 403 },
});
await store.dispatch(updateIsFavorite({ id: TimelineId.test, isFavorite: true }));

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { get } from 'lodash/fp';
import type { Action, Middleware } from 'redux';
import type { CoreStart } from '@kbn/core/public';
@ -17,12 +16,11 @@ import {
startTimelineSaving,
showCallOutUnauthorizedMsg,
} from '../actions';
import type { FavoriteTimelineResponse } from '../../../../common/api/timeline';
import { TimelineTypeEnum } from '../../../../common/api/timeline';
import { persistFavorite } from '../../containers/api';
import { selectTimelineById } from '../selectors';
import * as i18n from '../../pages/translations';
import { refreshTimelines } from './helpers';
import { isHttpFetchError, refreshTimelines } from './helpers';
type FavoriteTimelineAction = ReturnType<typeof updateIsFavorite>;
@ -42,19 +40,13 @@ export const favoriteTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, S
store.dispatch(startTimelineSaving({ id }));
try {
const result = await persistFavorite({
const response = await persistFavorite({
timelineId: timeline.id,
templateTimelineId: timeline.templateTimelineId,
templateTimelineVersion: timeline.templateTimelineVersion,
timelineType: timeline.timelineType ?? TimelineTypeEnum.default,
});
const response: FavoriteTimelineResponse = get('data.persistFavorite', result);
if (response.code === 403) {
store.dispatch(showCallOutUnauthorizedMsg());
}
refreshTimelines(store.getState());
store.dispatch(
@ -69,10 +61,14 @@ export const favoriteTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, S
})
);
} catch (error) {
kibana.notifications.toasts.addDanger({
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
});
if (isHttpFetchError(error) && error.body?.status_code === 403) {
store.dispatch(showCallOutUnauthorizedMsg());
} else {
kibana.notifications.toasts.addDanger({
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
});
}
} finally {
store.dispatch(
endTimelineSaving({

View file

@ -70,14 +70,8 @@ describe('Timeline note middleware', () => {
it('should persist a timeline note', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
},
},
note: {
noteId: testNote.id,
},
});
expect(selectTimelineById(store.getState(), TimelineId.test).noteIds).toEqual([]);
@ -92,14 +86,8 @@ describe('Timeline note middleware', () => {
it('should persist a note on an event of a timeline', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
},
},
note: {
noteId: testNote.id,
},
});
expect(selectTimelineById(store.getState(), TimelineId.test).eventIdToNoteIds).toEqual({
@ -123,14 +111,8 @@ describe('Timeline note middleware', () => {
it('should ensure the timeline is saved or in draft mode before creating a note', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
},
},
note: {
noteId: testNote.id,
},
});
@ -159,15 +141,9 @@ describe('Timeline note middleware', () => {
it('should pin the event when the event is not pinned yet', async () => {
const testTimelineId = 'testTimelineId';
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
timelineId: testTimelineId,
},
},
note: {
noteId: testNote.id,
timelineId: testTimelineId,
},
});
@ -207,15 +183,9 @@ describe('Timeline note middleware', () => {
);
const testTimelineId = 'testTimelineId';
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
timelineId: testTimelineId,
},
},
note: {
noteId: testNote.id,
timelineId: testTimelineId,
},
});
@ -232,12 +202,8 @@ describe('Timeline note middleware', () => {
});
it('should show an error message when the call is unauthorized', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 403,
},
},
(persistNote as jest.Mock).mockRejectedValue({
body: { status_code: 403 },
});
await store.dispatch(updateNote({ note: testNote }));

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { get } from 'lodash/fp';
import type { Action, Middleware } from 'redux';
import type { CoreStart } from '@kbn/core/public';
@ -22,10 +21,9 @@ import {
pinEvent,
} from '../actions';
import { persistNote } from '../../containers/notes/api';
import type { ResponseNote } from '../../../../common/api/timeline';
import { selectTimelineById } from '../selectors';
import * as i18n from '../../pages/translations';
import { ensureTimelineIsSaved, refreshTimelines } from './helpers';
import { ensureTimelineIsSaved, isHttpFetchError, refreshTimelines } from './helpers';
type NoteAction = ReturnType<typeof addNote | typeof addNoteToEvent>;
@ -64,7 +62,7 @@ export const addNoteToTimelineMiddleware: (kibana: CoreStart) => Middleware<{},
throw new Error('Cannot create note without a timelineId');
}
const result = await persistNote({
const response = await persistNote({
noteId: null,
version: null,
note: {
@ -74,11 +72,6 @@ export const addNoteToTimelineMiddleware: (kibana: CoreStart) => Middleware<{},
},
});
const response: ResponseNote = get('data.persistNote', result);
if (response.code === 403) {
store.dispatch(showCallOutUnauthorizedMsg());
}
refreshTimelines(store.getState());
await store.dispatch(
@ -112,10 +105,14 @@ export const addNoteToTimelineMiddleware: (kibana: CoreStart) => Middleware<{},
}
}
} catch (error) {
kibana.notifications.toasts.addDanger({
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
});
if (isHttpFetchError(error) && error.body?.status_code === 403) {
store.dispatch(showCallOutUnauthorizedMsg());
} else {
kibana.notifications.toasts.addDanger({
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
});
}
} finally {
store.dispatch(
endTimelineSaving({

View file

@ -63,12 +63,7 @@ describe('Timeline pinned event middleware', () => {
it('should persist a timeline pin event action', async () => {
(persistPinnedEvent as jest.Mock).mockResolvedValue({
data: {
persistPinnedEventOnTimeline: {
code: 200,
eventId: testEventId,
},
},
eventId: testEventId,
});
expect(selectTimelineById(store.getState(), TimelineId.test).pinnedEventIds).toEqual({});
await store.dispatch(pinEvent({ id: TimelineId.test, eventId: testEventId }));
@ -103,7 +98,7 @@ describe('Timeline pinned event middleware', () => {
);
(persistPinnedEvent as jest.Mock).mockResolvedValue({
data: {},
unpinned: true,
});
expect(selectTimelineById(store.getState(), TimelineId.test).pinnedEventIds).toEqual({
[testEventId]: true,
@ -117,13 +112,7 @@ describe('Timeline pinned event middleware', () => {
});
it('should ensure the timeline is saved or in draft mode before pinning an event', async () => {
(persistPinnedEvent as jest.Mock).mockResolvedValue({
data: {
persistPinnedEventOnTimeline: {
code: 200,
},
},
});
(persistPinnedEvent as jest.Mock).mockResolvedValue({});
expect(selectTimelineById(store.getState(), TimelineId.test).pinnedEventIds).toEqual({});
await store.dispatch(pinEvent({ id: TimelineId.test, eventId: testEventId }));
@ -139,12 +128,8 @@ describe('Timeline pinned event middleware', () => {
});
it('should show an error message when the call is unauthorized', async () => {
(persistPinnedEvent as jest.Mock).mockResolvedValue({
data: {
persistPinnedEventOnTimeline: {
code: 403,
},
},
(persistPinnedEvent as jest.Mock).mockRejectedValue({
body: { status_code: 403 },
});
await store.dispatch(unPinEvent({ id: TimelineId.test, eventId: testEventId }));

View file

@ -21,7 +21,7 @@ import {
showCallOutUnauthorizedMsg,
} from '../actions';
import { persistPinnedEvent } from '../../containers/pinned_event/api';
import { ensureTimelineIsSaved, refreshTimelines } from './helpers';
import { ensureTimelineIsSaved, isHttpFetchError, refreshTimelines } from './helpers';
type PinnedEventAction = ReturnType<typeof pinEvent | typeof unPinEvent>;
@ -55,7 +55,7 @@ export const addPinnedEventToTimelineMiddleware: (kibana: CoreStart) => Middlewa
throw new Error('Cannot create a pinned event without a timelineId');
}
const result = await persistPinnedEvent({
const response = await persistPinnedEvent({
pinnedEventId:
timeline.pinnedEventsSaveObject[eventId] != null
? timeline.pinnedEventsSaveObject[eventId].pinnedEventId
@ -64,17 +64,10 @@ export const addPinnedEventToTimelineMiddleware: (kibana: CoreStart) => Middlewa
timelineId: timeline.savedObjectId,
});
const response = result.data.persistPinnedEventOnTimeline;
if (response && 'code' in response && response.code === 403) {
store.dispatch(showCallOutUnauthorizedMsg());
}
refreshTimelines(store.getState());
const currentTimeline = selectTimelineById(store.getState(), action.payload.id);
// The response is null or empty in case we unpinned an event.
// In that case we want to remove the locally pinned event.
if (!response || !('eventId' in response)) {
if ('unpinned' in response) {
return store.dispatch(
updateTimeline({
id: action.payload.id,
@ -106,10 +99,14 @@ export const addPinnedEventToTimelineMiddleware: (kibana: CoreStart) => Middlewa
);
}
} catch (error) {
kibana.notifications.toasts.addDanger({
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
});
if (isHttpFetchError(error) && error.body?.status_code === 403) {
store.dispatch(showCallOutUnauthorizedMsg());
} else {
kibana.notifications.toasts.addDanger({
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
text: error?.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
});
}
} finally {
store.dispatch(
endTimelineSaving({

View file

@ -58,16 +58,8 @@ describe('Timeline save middleware', () => {
it('should persist a timeline', async () => {
(persistTimeline as jest.Mock).mockResolvedValue({
data: {
persistTimeline: {
code: 200,
message: 'success',
timeline: {
savedObjectId: 'soid',
version: 'newVersion',
},
},
},
savedObjectId: 'soid',
version: 'newVersion',
});
await store.dispatch(setChanged({ id: TimelineId.test, changed: true }));
expect(selectTimelineById(store.getState(), TimelineId.test)).toEqual(
@ -92,16 +84,8 @@ describe('Timeline save middleware', () => {
it('should copy a timeline', async () => {
(copyTimeline as jest.Mock).mockResolvedValue({
data: {
persistTimeline: {
code: 200,
message: 'success',
timeline: {
savedObjectId: 'soid',
version: 'newVersion',
},
},
},
savedObjectId: 'soid',
version: 'newVersion',
});
await store.dispatch(setChanged({ id: TimelineId.test, changed: true }));
expect(selectTimelineById(store.getState(), TimelineId.test)).toEqual(

View file

@ -63,7 +63,7 @@ export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State
store.dispatch(startTimelineSaving({ id: localTimelineId }));
try {
const result = await (action.payload.saveAsNew && timeline.id
const response = await (action.payload.saveAsNew && timeline.id
? copyTimeline({
timelineId,
timeline: {
@ -84,8 +84,8 @@ export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State
savedSearch: timeline.savedSearch,
}));
if (isTimelineErrorResponse(result)) {
const error = getErrorFromResponse(result);
if (isTimelineErrorResponse(response)) {
const error = getErrorFromResponse(response);
switch (error?.errorCode) {
case 403:
store.dispatch(showCallOutUnauthorizedMsg());
@ -106,7 +106,6 @@ export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State
return;
}
const response = result?.data?.persistTimeline;
if (response == null) {
kibana.notifications.toasts.addDanger({
title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
@ -122,15 +121,15 @@ export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State
id: localTimelineId,
timeline: {
...timeline,
id: response.timeline.savedObjectId,
updated: response.timeline.updated ?? undefined,
savedObjectId: response.timeline.savedObjectId,
version: response.timeline.version,
status: response.timeline.status ?? TimelineStatusEnum.active,
timelineType: response.timeline.timelineType ?? TimelineTypeEnum.default,
templateTimelineId: response.timeline.templateTimelineId ?? null,
templateTimelineVersion: response.timeline.templateTimelineVersion ?? null,
savedSearchId: response.timeline.savedSearchId ?? null,
id: response.savedObjectId,
updated: response.updated ?? undefined,
savedObjectId: response.savedObjectId,
version: response.version,
status: response.status ?? TimelineStatusEnum.active,
timelineType: response.timelineType ?? TimelineTypeEnum.default,
templateTimelineId: response.templateTimelineId ?? null,
templateTimelineVersion: response.templateTimelineVersion ?? null,
savedSearchId: response.savedSearchId ?? null,
isSaving: false,
},
})

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../common/api/timeline';
export const mockTemplate = {
columns: [
{
@ -172,13 +174,13 @@ export const mockTemplate = {
kqlQuery: { filterQuery: { kuery: { kind: 'kuery', expression: '' }, serializedQuery: '' } },
indexNames: [],
title: 'Generic Process Timeline - Duplicate - Duplicate',
timelineType: 'template',
timelineType: TimelineTypeEnum.template,
templateTimelineVersion: null,
templateTimelineId: null,
dateRange: { start: '2020-10-01T11:37:31.655Z', end: '2020-10-02T11:37:31.655Z' },
savedQueryId: null,
sort: { columnId: '@timestamp', sortDirection: 'desc' },
status: 'active',
status: TimelineStatusEnum.active,
};
export const mockTimeline = {
@ -210,11 +212,11 @@ export const mockTimeline = {
'.siem-signals-angelachuang-default',
],
title: 'my timeline',
timelineType: 'default',
timelineType: TimelineTypeEnum.default,
templateTimelineVersion: null,
templateTimelineId: null,
dateRange: { start: '2020-11-03T13:34:40.339Z', end: '2020-11-04T13:34:40.339Z' },
savedQueryId: null,
sort: { columnId: '@timestamp', columnType: 'number', sortDirection: 'desc' },
status: 'draft',
status: TimelineStatusEnum.draft,
};

View file

@ -92,13 +92,7 @@ describe('clean draft timelines', () => {
timelineType: req.body.timelineType,
});
expect(response.status).toEqual(200);
expect(response.body).toEqual({
data: {
persistTimeline: {
timeline: createTimelineWithTimelineId,
},
},
});
expect(response.body).toEqual(createTimelineWithTimelineId);
});
test('should return clean existing draft if draft available ', async () => {
@ -121,12 +115,6 @@ describe('clean draft timelines', () => {
expect(mockGetTimeline).toHaveBeenCalled();
expect(response.status).toEqual(200);
expect(response.body).toEqual({
data: {
persistTimeline: {
timeline: mockGetDraftTimelineValue,
},
},
});
expect(response.body).toEqual(mockGetDraftTimelineValue);
});
});

View file

@ -66,13 +66,7 @@ export const cleanDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) =
);
return response.ok({
body: {
data: {
persistTimeline: {
timeline: cleanedDraftTimeline,
},
},
},
body: cleanedDraftTimeline,
});
}
const templateTimelineData =
@ -91,17 +85,14 @@ export const cleanDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) =
if (newTimelineResponse.code === 200) {
return response.ok({
body: {
data: {
persistTimeline: {
timeline: newTimelineResponse.timeline,
},
},
},
body: newTimelineResponse.timeline,
});
} else {
return siemResponse.error({
body: newTimelineResponse.message,
statusCode: newTimelineResponse.code,
});
}
return response.ok({});
} catch (err) {
const error = transformError(err);

View file

@ -90,13 +90,7 @@ describe('get draft timelines', () => {
});
expect(response.status).toEqual(200);
expect(response.body).toEqual({
data: {
persistTimeline: {
timeline: createTimelineWithTimelineId,
},
},
});
expect(response.body).toEqual(createTimelineWithTimelineId);
});
test('should return an existing draft if available', async () => {
@ -110,13 +104,7 @@ describe('get draft timelines', () => {
);
expect(mockPersistTimeline).not.toHaveBeenCalled();
expect(response.status).toEqual(200);
expect(response.body).toEqual({
data: {
persistTimeline: {
timeline: mockGetDraftTimelineValue,
},
},
});
expect(response.body).toEqual(mockGetDraftTimelineValue);
});
});
});

View file

@ -49,13 +49,7 @@ export const getDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) =>
if (draftTimeline?.savedObjectId) {
return response.ok({
body: {
data: {
persistTimeline: {
timeline: draftTimeline,
},
},
},
body: draftTimeline,
});
}
@ -65,18 +59,13 @@ export const getDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) =>
});
if (newTimelineResponse.code === 200) {
return response.ok({
body: {
data: {
persistTimeline: {
timeline: newTimelineResponse.timeline,
},
},
},
return response.ok({ body: newTimelineResponse.timeline });
} else {
return siemResponse.error({
body: newTimelineResponse.message,
statusCode: newTimelineResponse.code,
});
}
return response.ok({});
} catch (err) {
const error = transformError(err);

View file

@ -15,7 +15,7 @@ import { NOTE_URL } from '../../../../../common/constants';
import { buildSiemResponse } from '../../../detection_engine/routes/utils';
import { buildFrameworkRequest } from '../../utils/common';
import { DeleteNoteRequestBody, type DeleteNoteResponse } from '../../../../../common/api/timeline';
import { DeleteNoteRequestBody } from '../../../../../common/api/timeline';
import { deleteNote } from '../../saved_object/notes';
export const deleteNoteRoute = (router: SecuritySolutionPluginRouter) => {
@ -36,7 +36,7 @@ export const deleteNoteRoute = (router: SecuritySolutionPluginRouter) => {
},
version: '2023-10-31',
},
async (context, request, response): Promise<IKibanaResponse<DeleteNoteResponse>> => {
async (context, request, response): Promise<IKibanaResponse> => {
const siemResponse = buildSiemResponse(response);
try {
@ -56,9 +56,7 @@ export const deleteNoteRoute = (router: SecuritySolutionPluginRouter) => {
noteIds,
});
return response.ok({
body: { data: {} },
});
return response.ok();
} catch (err) {
const error = transformError(err);
return siemResponse.error({

View file

@ -76,8 +76,7 @@ export const getNotesRoute = (
perPage: maxUnassociatedNotes,
};
const res = await getAllSavedNote(frameworkRequest, options);
const body: GetNotesResponse = res ?? {};
return response.ok({ body });
return response.ok({ body: res });
}
// searching for all the notes associated with a specific document id
@ -88,7 +87,7 @@ export const getNotesRoute = (
perPage: maxUnassociatedNotes,
};
const res = await getAllSavedNote(frameworkRequest, options);
return response.ok({ body: res ?? {} });
return response.ok({ body: res });
}
// if savedObjectIds is provided, we will search for all the notes associated with the savedObjectIds
@ -106,8 +105,7 @@ export const getNotesRoute = (
perPage: maxUnassociatedNotes,
};
const res = await getAllSavedNote(frameworkRequest, options);
const body: GetNotesResponse = res ?? {};
return response.ok({ body });
return response.ok({ body: res });
}
// searching for all the notes associated with a specific saved object id
@ -120,8 +118,7 @@ export const getNotesRoute = (
perPage: maxUnassociatedNotes,
};
const res = await getAllSavedNote(frameworkRequest, options);
const body: GetNotesResponse = res ?? {};
return response.ok({ body });
return response.ok({ body: res });
}
// retrieving all the notes following the query parameters
@ -236,8 +233,7 @@ export const getNotesRoute = (
options.filter = nodeBuilder.and(filterKueryNodeArray);
const res = await getAllSavedNote(frameworkRequest, options);
const body: GetNotesResponse = res ?? {};
return response.ok({ body });
return response.ok({ body: res });
} catch (err) {
const error = transformError(err);
const siemResponse = buildSiemResponse(response);

View file

@ -53,10 +53,9 @@ export const persistNoteRoute = (router: SecuritySolutionPluginRouter) => {
note,
overrideOwner: true,
});
const body: PersistNoteRouteResponse = { data: { persistNote: res } };
return response.ok({
body,
body: res,
});
} catch (err) {
const error = transformError(err);

View file

@ -61,9 +61,7 @@ export const persistPinnedEventRoute = (router: SecuritySolutionPluginRouter) =>
);
return response.ok({
body: {
data: { persistPinnedEventOnTimeline: res },
},
body: res,
});
} catch (err) {
const error = transformError(err);

View file

@ -44,9 +44,17 @@ export const copyTimelineRoute = (router: SecuritySolutionPluginRouter) => {
const frameworkRequest = await buildFrameworkRequest(context, request);
const { timeline, timelineIdToCopy } = request.body;
const copiedTimeline = await copyTimeline(frameworkRequest, timeline, timelineIdToCopy);
return response.ok({
body: { data: { persistTimeline: copiedTimeline } },
});
if (copiedTimeline.code === 200) {
return response.ok({
body: copiedTimeline.timeline,
});
} else {
return siemResponse.error({
body: copiedTimeline.message,
statusCode: copiedTimeline.code,
});
}
} catch (err) {
const error = transformError(err);
return siemResponse.error({

View file

@ -69,8 +69,8 @@ describe('create timelines', () => {
beforeEach(async () => {
jest.doMock('../../../saved_object/timelines', () => {
return {
getTimeline: mockGetTimeline.mockReturnValue(null),
persistTimeline: mockPersistTimeline.mockReturnValue({
code: 200,
timeline: createTimelineWithTimelineId,
}),
};
@ -173,6 +173,7 @@ describe('create timelines', () => {
return {
getTimelineTemplateOrNull: mockGetTimeline.mockReturnValue(null),
persistTimeline: mockPersistTimeline.mockReturnValue({
code: 200,
timeline: createTemplateTimelineWithTimelineId,
}),
};

View file

@ -81,13 +81,16 @@ export const createTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
timelineVersion: version,
});
return response.ok({
body: {
data: {
persistTimeline: newTimeline,
},
},
});
if (newTimeline.code === 200) {
return response.ok({
body: newTimeline.timeline,
});
} else {
return siemResponse.error({
statusCode: newTimeline.code,
body: newTimeline.message,
});
}
} else {
return siemResponse.error(
compareTimelinesStatus.checkIsFailureCases(TimelineStatusActions.create) || {

View file

@ -8,10 +8,7 @@
import type { IKibanaResponse } from '@kbn/core-http-server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import {
DeleteTimelinesRequestBody,
type DeleteTimelinesResponse,
} from '../../../../../../common/api/timeline';
import { DeleteTimelinesRequestBody } from '../../../../../../common/api/timeline';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { TIMELINE_URL } from '../../../../../../common/constants';
import { buildSiemResponse } from '../../../../detection_engine/routes/utils';
@ -37,7 +34,7 @@ export const deleteTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
request: { body: buildRouteValidationWithZod(DeleteTimelinesRequestBody) },
},
},
async (context, request, response): Promise<IKibanaResponse<DeleteTimelinesResponse>> => {
async (context, request, response): Promise<IKibanaResponse> => {
const siemResponse = buildSiemResponse(response);
try {
@ -45,8 +42,7 @@ export const deleteTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
const { savedObjectIds, searchIds } = request.body;
await deleteTimeline(frameworkRequest, savedObjectIds, searchIds);
const body: DeleteTimelinesResponse = { data: { deleteTimeline: true } };
return response.ok({ body });
return response.ok();
} catch (err) {
const error = transformError(err);
return siemResponse.error({

View file

@ -20,7 +20,6 @@ import {
type GetTimelineResponse,
} from '../../../../../../common/api/timeline';
import { getTimelineTemplateOrNull, getTimelineOrNull } from '../../../saved_object/timelines';
import type { ResolvedTimeline, TimelineResponse } from '../../../../../../common/api/timeline';
export const getTimelineRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
@ -41,26 +40,33 @@ export const getTimelineRoute = (router: SecuritySolutionPluginRouter) => {
},
},
async (context, request, response): Promise<IKibanaResponse<GetTimelineResponse>> => {
const siemResponse = buildSiemResponse(response);
try {
const frameworkRequest = await buildFrameworkRequest(context, request);
const query = request.query ?? {};
const { template_timeline_id: templateTimelineId, id } = query;
let res: TimelineResponse | ResolvedTimeline | null = null;
if (templateTimelineId != null && id == null) {
res = await getTimelineTemplateOrNull(frameworkRequest, templateTimelineId);
const timeline = await getTimelineTemplateOrNull(frameworkRequest, templateTimelineId);
if (timeline) {
return response.ok({ body: timeline });
}
} else if (templateTimelineId == null && id != null) {
res = await getTimelineOrNull(frameworkRequest, id);
const timelineOrNull = await getTimelineOrNull(frameworkRequest, id);
if (timelineOrNull) {
return response.ok({ body: timelineOrNull });
}
} else {
throw new Error('please provide id or template_timeline_id');
}
return response.ok({ body: res ? { data: { getOneTimeline: res } } : {} });
return siemResponse.error({
statusCode: 404,
body: 'Could not find timeline',
});
} catch (err) {
const error = transformError(err);
const siemResponse = buildSiemResponse(response);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,

View file

@ -59,7 +59,6 @@ export const getTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
sortOrder,
}
: null;
let res = null;
let totalCount = null;
if (pageSize == null && pageIndex == null) {
@ -75,7 +74,7 @@ export const getTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
totalCount = allActiveTimelines.totalCount;
}
res = await getAllTimeline(
const res = await getAllTimeline(
frameworkRequest,
onlyUserFavorite,
{
@ -88,7 +87,7 @@ export const getTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
timelineType
);
return response.ok({ body: res ?? {} });
return response.ok({ body: res });
} catch (err) {
const error = transformError(err);
const siemResponse = buildSiemResponse(response);

View file

@ -83,7 +83,7 @@ export const importTimelinesRoute = (router: SecuritySolutionPluginRouter, confi
if (res instanceof Error || typeof res === 'string') {
throw res;
} else {
return response.ok({ body: res ?? {} });
return response.ok({ body: res });
}
} catch (err) {
const error = transformError(err);

View file

@ -69,6 +69,7 @@ describe('update timelines', () => {
getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
persistTimeline: mockPersistTimeline.mockReturnValue({
timeline: updateTimelineWithTimelineId.timeline,
code: 200,
}),
};
});
@ -177,6 +178,7 @@ describe('update timelines', () => {
}),
persistTimeline: mockPersistTimeline.mockReturnValue({
timeline: updateTemplateTimelineWithTimelineId.timeline,
code: 200,
}),
};
});

View file

@ -73,13 +73,16 @@ export const patchTimelinesRoute = (router: SecuritySolutionPluginRouter) => {
timelineVersion: version,
});
return response.ok({
body: {
data: {
persistTimeline: updatedTimeline,
},
},
});
if (updatedTimeline.code === 200) {
return response.ok({
body: updatedTimeline.timeline,
});
} else {
return siemResponse.error({
statusCode: updatedTimeline.code,
body: updatedTimeline.message,
});
}
} else {
const error = compareTimelinesStatus.checkIsFailureCases(TimelineStatusActions.update);
return siemResponse.error(

View file

@ -52,7 +52,7 @@ export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter) => {
const { timelineId, templateTimelineId, templateTimelineVersion, timelineType } =
request.body;
const timeline = await persistFavorite(
const persistFavoriteResponse = await persistFavorite(
frameworkRequest,
timelineId || null,
templateTimelineId || null,
@ -60,15 +60,16 @@ export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter) => {
timelineType || TimelineTypeEnum.default
);
const body: PersistFavoriteRouteResponse = {
data: {
persistFavorite: timeline,
},
};
return response.ok({
body,
});
if (persistFavoriteResponse.code !== 200) {
return siemResponse.error({
body: persistFavoriteResponse.message,
statusCode: persistFavoriteResponse.code,
});
} else {
return response.ok({
body: persistFavoriteResponse.favoriteTimeline,
});
}
} catch (err) {
const error = transformError(err);
return siemResponse.error({

View file

@ -21,7 +21,6 @@ import {
type ResolveTimelineResponse,
} from '../../../../../../common/api/timeline';
import { getTimelineTemplateOrNull, resolveTimelineOrNull } from '../../../saved_object/timelines';
import type { SavedTimeline, ResolvedTimeline } from '../../../../../../common/api/timeline';
export const resolveTimelineRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
@ -42,28 +41,39 @@ export const resolveTimelineRoute = (router: SecuritySolutionPluginRouter) => {
},
},
async (context, request, response): Promise<IKibanaResponse<ResolveTimelineResponse>> => {
const siemResponse = buildSiemResponse(response);
try {
const frameworkRequest = await buildFrameworkRequest(context, request);
const query = request.query ?? {};
const { template_timeline_id: templateTimelineId, id } = query;
let res: SavedTimeline | ResolvedTimeline | null = null;
if (templateTimelineId != null && id == null) {
// Template timelineId is not a SO id, so it does not need to be updated to use resolve
res = await getTimelineTemplateOrNull(frameworkRequest, templateTimelineId);
const timeline = await getTimelineTemplateOrNull(frameworkRequest, templateTimelineId);
if (timeline) {
return response.ok({
body: { timeline, outcome: 'exactMatch' },
});
}
} else if (templateTimelineId == null && id != null) {
// In the event the objectId is defined, run the resolve call
res = await resolveTimelineOrNull(frameworkRequest, id);
const timelineOrNull = await resolveTimelineOrNull(frameworkRequest, id);
if (timelineOrNull) {
return response.ok({
body: timelineOrNull,
});
}
} else {
throw new Error('please provide id or template_timeline_id');
}
return response.ok({ body: res ? { data: res } : {} });
return siemResponse.error({
statusCode: 404,
body: 'Could not resolve timeline',
});
} catch (err) {
const error = transformError(err);
const siemResponse = buildSiemResponse(response);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,

View file

@ -6,7 +6,7 @@
*/
import type { FrameworkRequest } from '../../../framework';
import { persistNote } from './saved_object';
import { persistNote, type InternalNoteResponse } from './saved_object';
import { getOverridableNote } from './get_overridable_note';
import type { Note } from '../../../../../common/api/timeline';
@ -16,7 +16,7 @@ export const persistNotes = async (
existingNoteIds?: string[] | null,
newNotes?: Note[],
overrideOwner: boolean = true
) => {
): Promise<InternalNoteResponse[]> => {
return Promise.all(
newNotes?.map(async (note) => {
const newNote = await getOverridableNote(
@ -31,6 +31,6 @@ export const persistNotes = async (
note: newNote,
overrideOwner,
});
}) ?? []
}) ?? ([] as InternalNoteResponse[])
);
};

View file

@ -6,7 +6,6 @@
*/
import { failure } from 'io-ts/lib/PathReporter';
import { getOr } from 'lodash/fp';
import { v1 as uuidv1 } from 'uuid';
import { pipe } from 'fp-ts/lib/pipeable';
@ -81,6 +80,11 @@ export const getNotesByTimelineId = async (
return notesByTimelineId.notes;
};
export interface InternalNoteResponse extends ResponseNote {
message: string;
code: number;
}
export const persistNote = async ({
request,
noteId,
@ -91,35 +95,18 @@ export const persistNote = async ({
noteId: string | null;
note: BareNote | BareNoteWithoutExternalRefs;
overrideOwner?: boolean;
}): Promise<ResponseNote> => {
try {
if (noteId == null) {
return await createNote({
request,
noteId,
note,
overrideOwner,
});
}
// Update existing note
return await updateNote({ request, noteId, note, overrideOwner });
} catch (err) {
if (getOr(null, 'output.statusCode', err) === 403) {
const noteToReturn: Note = {
...note,
noteId: uuidv1(),
version: '',
timelineId: '',
};
return {
code: 403,
message: err.message,
note: noteToReturn,
};
}
throw err;
}): Promise<InternalNoteResponse> => {
if (noteId == null) {
return createNote({
request,
noteId,
note,
overrideOwner,
});
}
// Update existing note
return updateNote({ request, noteId, note, overrideOwner });
};
export const createNote = async ({
@ -132,7 +119,7 @@ export const createNote = async ({
noteId: string | null;
note: BareNote | BareNoteWithoutExternalRefs;
overrideOwner?: boolean;
}): Promise<ResponseNote> => {
}): Promise<InternalNoteResponse> => {
const {
savedObjects: { client: savedObjectsClient },
uiSettings: { client: uiSettingsClient },
@ -203,7 +190,7 @@ export const updateNote = async ({
noteId: string;
note: BareNote | BareNoteWithoutExternalRefs;
overrideOwner?: boolean;
}): Promise<ResponseNote> => {
}): Promise<InternalNoteResponse> => {
const savedObjectsClient = (await request.context.core).savedObjects.client;
const userInfo = request.user;

View file

@ -6,7 +6,6 @@
*/
import { failure } from 'io-ts/lib/PathReporter';
import { getOr } from 'lodash/fp';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
@ -77,44 +76,24 @@ export const persistPinnedEventOnTimeline = async (
eventId: string,
timelineId: string
): Promise<PersistPinnedEventResponse> => {
try {
if (pinnedEventId != null) {
// Delete Pinned Event on Timeline
await deletePinnedEventOnTimeline(request, [pinnedEventId]);
return null;
}
const pinnedEvents = await getPinnedEventsInTimelineWithEventId(request, timelineId, eventId);
// we already had this event pinned so let's just return the one we already had
if (pinnedEvents.length > 0) {
return { ...pinnedEvents[0], code: 200 };
}
return await createPinnedEvent({
request,
eventId,
timelineId,
});
} catch (err) {
if (getOr(null, 'output.statusCode', err) === 404) {
/*
* Why we are doing that, because if it is not found for sure that it will be unpinned
* There is no need to bring back this error since we can assume that it is unpinned
*/
return null;
}
if (getOr(null, 'output.statusCode', err) === 403) {
return pinnedEventId != null
? {
code: 403,
message: err.message,
pinnedEventId: eventId,
}
: null;
}
throw err;
if (pinnedEventId != null) {
// Delete Pinned Event on Timeline
await deletePinnedEventOnTimeline(request, [pinnedEventId]);
return { unpinned: true };
}
const pinnedEvents = await getPinnedEventsInTimelineWithEventId(request, timelineId, eventId);
// we already had this event pinned so let's just return the one we already had
if (pinnedEvents.length > 0) {
return { ...pinnedEvents[0] };
}
return createPinnedEvent({
request,
eventId,
timelineId,
});
};
const getPinnedEventsInTimelineWithEventId = async (
@ -172,7 +151,6 @@ const createPinnedEvent = async ({
// create Pinned Event on Timeline
return {
...convertSavedObjectToSavedPinnedEvent(repopulatedSavedObject),
code: 200,
};
};

View file

@ -298,61 +298,67 @@ export const getDraftTimeline = async (
return getAllSavedTimeline(request, options);
};
interface InternalPersistFavoriteResponse {
code: number;
message: string;
favoriteTimeline: FavoriteTimelineResponse;
}
export const persistFavorite = async (
request: FrameworkRequest,
timelineId: string | null,
templateTimelineId: string | null,
templateTimelineVersion: number | null,
timelineType: TimelineType
): Promise<FavoriteTimelineResponse> => {
): Promise<InternalPersistFavoriteResponse> => {
const userName = request.user?.username ?? UNAUTHENTICATED_USER;
const fullName = request.user?.full_name ?? '';
try {
let timeline: SavedTimeline = {};
if (timelineId != null) {
const {
eventIdToNoteIds,
notes,
noteIds,
pinnedEventIds,
pinnedEventsSaveObject,
savedObjectId,
version,
...savedTimeline
} = await getBasicSavedTimeline(request, timelineId);
timelineId = savedObjectId; // eslint-disable-line no-param-reassign
timeline = savedTimeline;
}
let timeline: SavedTimeline = {};
if (timelineId != null) {
const {
eventIdToNoteIds,
notes,
noteIds,
pinnedEventIds,
pinnedEventsSaveObject,
savedObjectId,
version,
...savedTimeline
} = await getBasicSavedTimeline(request, timelineId);
timelineId = savedObjectId; // eslint-disable-line no-param-reassign
timeline = savedTimeline;
}
const userFavoriteTimeline = {
keySearch: userName != null ? convertStringToBase64(userName) : null,
favoriteDate: new Date().valueOf(),
fullName,
userName,
};
if (timeline.favorite != null) {
const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex(
(user) => user.userName === userName
);
const userFavoriteTimeline = {
keySearch: userName != null ? convertStringToBase64(userName) : null,
favoriteDate: new Date().valueOf(),
fullName,
userName,
};
if (timeline.favorite != null) {
const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex(
(user) => user.userName === userName
);
timeline.favorite =
alreadyExistsTimelineFavoriteByUser > -1
? [
...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser),
...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1),
]
: [...timeline.favorite, userFavoriteTimeline];
} else if (timeline.favorite == null) {
timeline.favorite = [userFavoriteTimeline];
}
timeline.favorite =
alreadyExistsTimelineFavoriteByUser > -1
? [
...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser),
...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1),
]
: [...timeline.favorite, userFavoriteTimeline];
} else if (timeline.favorite == null) {
timeline.favorite = [userFavoriteTimeline];
}
const persistResponse = await persistTimeline(request, timelineId, null, {
...timeline,
templateTimelineId,
templateTimelineVersion,
timelineType,
});
return {
const persistResponse = await persistTimeline(request, timelineId, null, {
...timeline,
templateTimelineId,
templateTimelineVersion,
timelineType,
});
return {
favoriteTimeline: {
savedObjectId: persistResponse.timeline.savedObjectId,
version: persistResponse.timeline.version,
favorite:
@ -362,19 +368,10 @@ export const persistFavorite = async (
templateTimelineId,
templateTimelineVersion,
timelineType,
};
} catch (err) {
if (getOr(null, 'output.statusCode', err) === 403) {
return {
savedObjectId: '',
version: '',
favorite: [],
code: 403,
message: err.message,
};
}
throw err;
}
},
code: persistResponse.code,
message: persistResponse.message,
};
};
export interface InternalTimelineResponse {

View file

@ -27,8 +27,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineType: 'default',
});
const { savedObjectId, version } =
response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId, version } = response.body.data && response.body;
expect(savedObjectId).to.not.be.empty();
expect(version).to.not.be.empty();
@ -49,7 +48,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineType,
templateTimelineId,
templateTimelineVersion,
} = response.body.data && response.body.data.persistTimeline.timeline;
} = response.body.data && response.body;
expect(savedObjectId).to.not.be.empty();
expect(version).to.not.be.empty();
@ -72,7 +71,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
pinnedEventIds: initialPinnedEventIds,
noteIds: initialNoteIds,
version: initialVersion,
} = response.body.data && response.body.data.persistTimeline.timeline;
} = response.body.data && response.body;
expect(initialPinnedEventIds).to.have.length(0, 'should not have any pinned events');
expect(initialNoteIds).to.have.length(0, 'should not have any notes');
@ -107,7 +106,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
pinnedEventIds,
noteIds,
status: newStatus,
} = getTimelineRequest.body.data && getTimelineRequest.body.data.getOneTimeline;
} = getTimelineRequest.body.data && getTimelineRequest.body;
expect(newStatus).to.be.equal('draft', 'status should still be draft');
expect(pinnedEventIds).to.have.length(1, 'should have one pinned event');
@ -126,8 +125,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
pinnedEventIds: cleanedPinnedEventIds,
noteIds: cleanedNoteIds,
version: cleanedVersion,
} = cleanDraftTimelineRequest.body.data &&
cleanDraftTimelineRequest.body.data.persistTimeline.timeline;
} = cleanDraftTimelineRequest.body.data && cleanDraftTimelineRequest.body;
expect(cleanedPinnedEventIds).to.have.length(0, 'should not have pinned events anymore');
expect(cleanedNoteIds).to.have.length(0, 'should not have notes anymore');

View file

@ -35,8 +35,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
note: { note: myNote, timelineId: 'testTimelineId' },
});
const { note, noteId, timelineId, version } =
response.body.data && response.body.data.persistNote.note;
const { note, noteId, timelineId, version } = response.body && response.body.note;
expect(note).to.be(myNote);
expect(noteId).to.not.be.empty();
@ -56,8 +55,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
note: { note: myNote, timelineId: 'testTimelineId' },
});
const { noteId, timelineId, version } =
response.body.data && response.body.data.persistNote.note;
const { noteId, timelineId, version } = response.body && response.body.note;
const myNewNote = 'new world test';
const responseToTest = await supertest
@ -70,9 +68,9 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
note: { note: myNewNote, timelineId },
});
expect(responseToTest.body.data!.persistNote.note.note).to.be(myNewNote);
expect(responseToTest.body.data!.persistNote.note.noteId).to.be(noteId);
expect(responseToTest.body.data!.persistNote.note.version).to.not.be.eql(version);
expect(responseToTest.body.note.note).to.be(myNewNote);
expect(responseToTest.body.note.noteId).to.be(noteId);
expect(responseToTest.body.note.version).to.not.be.eql(version);
});
});

View file

@ -28,8 +28,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineId: 'testId',
eventId: 'bv4QSGsB9v5HJNSH-7fi',
});
const { eventId, pinnedEventId, timelineId, version } =
response.body.data && response.body.data.persistPinnedEventOnTimeline;
const { eventId, pinnedEventId, timelineId, version } = response.body;
expect(eventId).to.be('bv4QSGsB9v5HJNSH-7fi');
expect(pinnedEventId).to.not.be.empty();
@ -39,7 +38,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
});
describe('unpin an event', () => {
it('returns null', async () => {
it('returns { unpinned: true }', async () => {
const response = await supertest
.patch(PINNED_EVENT_URL)
.set('elastic-api-version', '2023-10-31')
@ -49,8 +48,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
eventId: 'bv4QSGsB9v5HJNSH-7fi',
timelineId: 'testId',
});
const { eventId, pinnedEventId, timelineId } =
response.body.data && response.body.data.persistPinnedEventOnTimeline;
const { eventId, pinnedEventId, timelineId } = response.body;
const responseToTest = await supertest
.patch(PINNED_EVENT_URL)
@ -61,7 +59,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
eventId,
timelineId,
});
expect(responseToTest.body.data!.persistPinnedEventOnTimeline).to.be(null);
expect(responseToTest.body).to.eql({ unpinned: true });
});
});
});

View file

@ -6,10 +6,7 @@
*/
import expect from '@kbn/expect';
import {
TimelineResponse,
TimelineTypeEnum,
} from '@kbn/security-solution-plugin/common/api/timeline';
import { TimelineTypeEnum } from '@kbn/security-solution-plugin/common/api/timeline';
import { TIMELINE_URL } from '@kbn/security-solution-plugin/common/constants';
import TestAgent from 'supertest/lib/agent';
import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces';
@ -26,8 +23,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
it('Create a timeline just with a title', async () => {
const titleToSaved = 'hello title';
const response = await createBasicTimeline(supertest, titleToSaved);
const { savedObjectId, title, version } =
response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId, title, version } = response.body;
expect(title).to.be(titleToSaved);
expect(savedObjectId).to.not.be.empty();
@ -152,8 +148,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
sort,
title,
version,
} =
response.body.data && omitTypenameInTimeline(response.body.data.persistTimeline.timeline);
} = response.body;
expect(columns.map((col: { id: string }) => col.id)).to.eql(
timelineObject.columns.map((col) => col.id)
@ -172,8 +167,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
it('Update a timeline with a new title', async () => {
const titleToSaved = 'hello title';
const response = await createBasicTimeline(supertest, titleToSaved);
const { savedObjectId, version } =
response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId, version } = response.body;
const newTitle = 'new title';
@ -187,11 +181,9 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
title: newTitle,
},
});
expect(responseToTest.body.data!.persistTimeline.timeline.savedObjectId).to.eql(
savedObjectId
);
expect(responseToTest.body.data!.persistTimeline.timeline.title).to.be(newTitle);
expect(responseToTest.body.data!.persistTimeline.timeline.version).to.not.be.eql(version);
expect(responseToTest.body.savedObjectId).to.eql(savedObjectId);
expect(responseToTest.body.title).to.be(newTitle);
expect(responseToTest.body.version).to.not.be.eql(version);
});
});
@ -200,8 +192,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
const titleToSaved = 'hello title';
const response = await createBasicTimeline(supertest, titleToSaved);
const { savedObjectId, version } =
response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId, version } = response.body;
const responseToTest = await supertest
.patch('/api/timeline/_favorite')
@ -213,14 +204,12 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineType: TimelineTypeEnum.default,
});
expect(responseToTest.body.data!.persistFavorite.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.data!.persistFavorite.favorite.length).to.be(1);
expect(responseToTest.body.data!.persistFavorite.version).to.not.be.eql(version);
expect(responseToTest.body.data!.persistFavorite.templateTimelineId).to.be.eql(null);
expect(responseToTest.body.data!.persistFavorite.templateTimelineVersion).to.be.eql(null);
expect(responseToTest.body.data!.persistFavorite.timelineType).to.be.eql(
TimelineTypeEnum.default
);
expect(responseToTest.body.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.favorite.length).to.be(1);
expect(responseToTest.body.version).to.not.be.eql(version);
expect(responseToTest.body.templateTimelineId).to.be.eql(null);
expect(responseToTest.body.templateTimelineVersion).to.be.eql(null);
expect(responseToTest.body.timelineType).to.be.eql(TimelineTypeEnum.default);
});
it('to an existing timeline template', async () => {
@ -228,8 +217,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
const templateTimelineIdFromStore = 'f4a90a2d-365c-407b-9fef-c1dcb33a6ab3';
const templateTimelineVersionFromStore = 1;
const response = await createBasicTimeline(supertest, titleToSaved);
const { savedObjectId, version } =
response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId, version } = response.body;
const responseToTest = await supertest
.patch('/api/timeline/_favorite')
@ -240,25 +228,20 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
templateTimelineVersion: templateTimelineVersionFromStore,
timelineType: TimelineTypeEnum.template,
});
expect(responseToTest.body.data!.persistFavorite.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.data!.persistFavorite.favorite.length).to.be(1);
expect(responseToTest.body.data!.persistFavorite.version).to.not.be.eql(version);
expect(responseToTest.body.data!.persistFavorite.templateTimelineId).to.be.eql(
templateTimelineIdFromStore
);
expect(responseToTest.body.data!.persistFavorite.templateTimelineVersion).to.be.eql(
expect(responseToTest.body.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.favorite.length).to.be(1);
expect(responseToTest.body.version).to.not.be.eql(version);
expect(responseToTest.body.templateTimelineId).to.be.eql(templateTimelineIdFromStore);
expect(responseToTest.body.templateTimelineVersion).to.be.eql(
templateTimelineVersionFromStore
);
expect(responseToTest.body.data!.persistFavorite.timelineType).to.be.eql(
TimelineTypeEnum.template
);
expect(responseToTest.body.timelineType).to.be.eql(TimelineTypeEnum.template);
});
it('to Unfavorite an existing timeline', async () => {
const titleToSaved = 'hello title';
const response = await createBasicTimeline(supertest, titleToSaved);
const { savedObjectId, version } =
response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId, version } = response.body;
await supertest.patch('/api/timeline/_favorite').set('kbn-xsrf', 'true').send({
timelineId: savedObjectId,
@ -277,14 +260,12 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineType: TimelineTypeEnum.default,
});
expect(responseToTest.body.data!.persistFavorite.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.data!.persistFavorite.favorite).to.be.empty();
expect(responseToTest.body.data!.persistFavorite.version).to.not.be.eql(version);
expect(responseToTest.body.data!.persistFavorite.templateTimelineId).to.be.eql(null);
expect(responseToTest.body.data!.persistFavorite.templateTimelineVersion).to.be.eql(null);
expect(responseToTest.body.data!.persistFavorite.timelineType).to.be.eql(
TimelineTypeEnum.default
);
expect(responseToTest.body.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.favorite).to.be.empty();
expect(responseToTest.body.version).to.not.be.eql(version);
expect(responseToTest.body.templateTimelineId).to.be.eql(null);
expect(responseToTest.body.templateTimelineVersion).to.be.eql(null);
expect(responseToTest.body.timelineType).to.be.eql(TimelineTypeEnum.default);
});
it('to Unfavorite an existing timeline template', async () => {
@ -292,8 +273,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
const templateTimelineIdFromStore = 'f4a90a2d-365c-407b-9fef-c1dcb33a6ab3';
const templateTimelineVersionFromStore = 1;
const response = await createBasicTimeline(supertest, titleToSaved);
const { savedObjectId, version } =
response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId, version } = response.body;
await supertest.patch('/api/timeline/_favorite').set('kbn-xsrf', 'true').send({
timelineId: savedObjectId,
@ -312,18 +292,14 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineType: TimelineTypeEnum.template,
});
expect(responseToTest.body.data!.persistFavorite.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.data!.persistFavorite.favorite).to.be.empty();
expect(responseToTest.body.data!.persistFavorite.version).to.not.be.eql(version);
expect(responseToTest.body.data!.persistFavorite.templateTimelineId).to.be.eql(
templateTimelineIdFromStore
);
expect(responseToTest.body.data!.persistFavorite.templateTimelineVersion).to.be.eql(
expect(responseToTest.body.savedObjectId).to.be(savedObjectId);
expect(responseToTest.body.favorite).to.be.empty();
expect(responseToTest.body.version).to.not.be.eql(version);
expect(responseToTest.body.templateTimelineId).to.be.eql(templateTimelineIdFromStore);
expect(responseToTest.body.templateTimelineVersion).to.be.eql(
templateTimelineVersionFromStore
);
expect(responseToTest.body.data!.persistFavorite.timelineType).to.be.eql(
TimelineTypeEnum.template
);
expect(responseToTest.body.timelineType).to.be.eql(TimelineTypeEnum.template);
});
it('to a timeline without a timelineId', async () => {
@ -337,14 +313,12 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineType: TimelineTypeEnum.default,
});
expect(response.body.data!.persistFavorite.savedObjectId).to.not.be.empty();
expect(response.body.data!.persistFavorite.favorite.length).to.be(1);
expect(response.body.data!.persistFavorite.version).to.not.be.empty();
expect(response.body.data!.persistFavorite.templateTimelineId).to.be.eql(null);
expect(response.body.data!.persistFavorite.templateTimelineVersion).to.be.eql(null);
expect(response.body.data!.persistFavorite.timelineType).to.be.eql(
TimelineTypeEnum.default
);
expect(response.body.savedObjectId).to.not.be.empty();
expect(response.body.favorite.length).to.be(1);
expect(response.body.version).to.not.be.empty();
expect(response.body.templateTimelineId).to.be.eql(null);
expect(response.body.templateTimelineVersion).to.be.eql(null);
expect(response.body.timelineType).to.be.eql(TimelineTypeEnum.default);
});
it('to a timeline template without a timelineId', async () => {
@ -361,18 +335,12 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
timelineType: TimelineTypeEnum.template,
});
expect(response.body.data!.persistFavorite.savedObjectId).to.not.be.empty();
expect(response.body.data!.persistFavorite.favorite.length).to.be(1);
expect(response.body.data!.persistFavorite.version).to.not.be.empty();
expect(response.body.data!.persistFavorite.templateTimelineId).to.be.eql(
templateTimelineIdFromStore
);
expect(response.body.data!.persistFavorite.templateTimelineVersion).to.be.eql(
templateTimelineVersionFromStore
);
expect(response.body.data!.persistFavorite.timelineType).to.be.eql(
TimelineTypeEnum.template
);
expect(response.body.savedObjectId).to.not.be.empty();
expect(response.body.favorite.length).to.be(1);
expect(response.body.version).to.not.be.empty();
expect(response.body.templateTimelineId).to.be.eql(templateTimelineIdFromStore);
expect(response.body.templateTimelineVersion).to.be.eql(templateTimelineVersionFromStore);
expect(response.body.timelineType).to.be.eql(TimelineTypeEnum.template);
});
});
@ -380,7 +348,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
it('one timeline', async () => {
const titleToSaved = 'hello title';
const response = await createBasicTimeline(supertest, titleToSaved);
const { savedObjectId } = response.body.data && response.body.data.persistTimeline.timeline;
const { savedObjectId } = response.body;
const responseToTest = await supertest
.delete(TIMELINE_URL)
@ -389,22 +357,16 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
savedObjectIds: [savedObjectId],
});
expect(responseToTest.body.data!.deleteTimeline).to.be(true);
expect(responseToTest.statusCode).to.be(200);
});
it('multiple timelines', async () => {
const titleToSaved = 'hello title';
const response1 = await createBasicTimeline(supertest, titleToSaved);
const savedObjectId1 =
response1.body.data && response1.body.data.persistTimeline.timeline
? response1.body.data.persistTimeline.timeline.savedObjectId
: '';
const savedObjectId1 = response1.body ? response1.body.savedObjectId : '';
const response2 = await createBasicTimeline(supertest, titleToSaved);
const savedObjectId2 =
response2.body.data && response2.body.data.persistTimeline.timeline
? response2.body.data.persistTimeline.timeline.savedObjectId
: '';
const savedObjectId2 = response2.body ? response2.body.savedObjectId : '';
const responseToTest = await supertest
.delete(TIMELINE_URL)
@ -413,14 +375,8 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
savedObjectIds: [savedObjectId1, savedObjectId2],
});
expect(responseToTest.body.data!.deleteTimeline).to.be(true);
expect(responseToTest.status).to.be(200);
});
});
});
}
const omitTypename = (key: string, value: keyof TimelineResponse) =>
key === '__typename' ? undefined : value;
const omitTypenameInTimeline = (timeline: TimelineResponse) =>
JSON.parse(JSON.stringify(timeline), omitTypename);

View file

@ -68,9 +68,9 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(resolveWithSpaceApi)
.query({ id: '1e2e9850-25f8-11ec-a981-b77847c6ef30' });
expect(resp.body.data.outcome).to.be('aliasMatch');
expect(resp.body.data.alias_target_id).to.not.be(undefined);
expect(resp.body.data.timeline.title).to.be('An awesome timeline');
expect(resp.body.outcome).to.be('aliasMatch');
expect(resp.body.alias_target_id).to.not.be(undefined);
expect(resp.body.timeline.title).to.be('An awesome timeline');
});
describe('notes', () => {
@ -79,7 +79,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(resolveWithSpaceApi)
.query({ id: '1e2e9850-25f8-11ec-a981-b77847c6ef30' });
expect(resp.body.data.timeline.notes[0].eventId).to.be('StU_UXwBAowmaxx6YdiS');
expect(resp.body.timeline.notes[0].eventId).to.be('StU_UXwBAowmaxx6YdiS');
});
it('should return notes with the timelineId matching the resolved timeline id', async () => {
@ -87,12 +87,8 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(resolveWithSpaceApi)
.query({ id: '1e2e9850-25f8-11ec-a981-b77847c6ef30' });
expect(resp.body.data.timeline.notes[0].timelineId).to.be(
resp.body.data.timeline.savedObjectId
);
expect(resp.body.data.timeline.notes[1].timelineId).to.be(
resp.body.data.timeline.savedObjectId
);
expect(resp.body.timeline.notes[0].timelineId).to.be(resp.body.timeline.savedObjectId);
expect(resp.body.timeline.notes[1].timelineId).to.be(resp.body.timeline.savedObjectId);
});
});
@ -102,7 +98,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(resolveWithSpaceApi)
.query({ id: '1e2e9850-25f8-11ec-a981-b77847c6ef30' });
expect(resp.body.data.timeline.pinnedEventsSaveObject[0].eventId).to.be(
expect(resp.body.timeline.pinnedEventsSaveObject[0].eventId).to.be(
'StU_UXwBAowmaxx6YdiS'
);
});
@ -112,8 +108,8 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(resolveWithSpaceApi)
.query({ id: '1e2e9850-25f8-11ec-a981-b77847c6ef30' });
expect(resp.body.data.timeline.pinnedEventsSaveObject[0].timelineId).to.be(
resp.body.data.timeline.savedObjectId
expect(resp.body.timeline.pinnedEventsSaveObject[0].timelineId).to.be(
resp.body.timeline.savedObjectId
);
});
});
@ -161,7 +157,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(TIMELINE_URL)
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
expect(resp.body.data.getOneTimeline.notes[0].eventId).to.be('Edo00XsBEVtyvU-8LGNe');
expect(resp.body.notes[0].eventId).to.be('Edo00XsBEVtyvU-8LGNe');
});
it('returns the timelineId in the response', async () => {
@ -169,12 +165,8 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(TIMELINE_URL)
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
expect(resp.body.data.getOneTimeline.notes[0].timelineId).to.be(
'6484cc90-126e-11ec-83d2-db1096c73738'
);
expect(resp.body.data.getOneTimeline.notes[1].timelineId).to.be(
'6484cc90-126e-11ec-83d2-db1096c73738'
);
expect(resp.body.notes[0].timelineId).to.be('6484cc90-126e-11ec-83d2-db1096c73738');
expect(resp.body.notes[1].timelineId).to.be('6484cc90-126e-11ec-83d2-db1096c73738');
});
});
@ -198,7 +190,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(TIMELINE_URL)
.query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' });
expect(resp.body.data.getOneTimeline.title).to.be('Awesome Timeline');
expect(resp.body.title).to.be('Awesome Timeline');
});
it('returns the savedQueryId in the response', async () => {
@ -206,7 +198,7 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(TIMELINE_URL)
.query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' });
expect(resp.body.data.getOneTimeline.savedQueryId).to.be("It's me");
expect(resp.body.savedQueryId).to.be("It's me");
});
});
describe('pinned events timelineId', () => {
@ -238,12 +230,8 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(TIMELINE_URL)
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[0].eventId).to.be(
'DNo00XsBEVtyvU-8LGNe'
);
expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[1].eventId).to.be(
'Edo00XsBEVtyvU-8LGNe'
);
expect(resp.body.pinnedEventsSaveObject[0].eventId).to.be('DNo00XsBEVtyvU-8LGNe');
expect(resp.body.pinnedEventsSaveObject[1].eventId).to.be('Edo00XsBEVtyvU-8LGNe');
});
it('returns the timelineId in the response', async () => {
@ -251,10 +239,10 @@ export default function ({ getService }: FtrProviderContextWithSpaces) {
.get(TIMELINE_URL)
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[0].timelineId).to.be(
expect(resp.body.pinnedEventsSaveObject[0].timelineId).to.be(
'6484cc90-126e-11ec-83d2-db1096c73738'
);
expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[1].timelineId).to.be(
expect(resp.body.pinnedEventsSaveObject[1].timelineId).to.be(
'6484cc90-126e-11ec-83d2-db1096c73738'
);
});

View file

@ -59,7 +59,7 @@ describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () =>
deleteAlertsAndRules();
createTimeline()
.then((response) => {
return response.body.data.persistTimeline.timeline.savedObjectId;
return response.body.savedObjectId;
})
.as('timelineId');
visit(CREATE_RULE_URL);

View file

@ -72,8 +72,8 @@ describe(
tags: ruleFields.ruleTags,
false_positives: ruleFields.falsePositives,
note: ruleFields.investigationGuide,
timeline_id: response.body.data.persistTimeline.timeline.savedObjectId,
timeline_title: response.body.data.persistTimeline.timeline.title ?? '',
timeline_id: response.body.savedObjectId,
timeline_title: response.body.title ?? '',
interval: ruleFields.ruleInterval,
from: `now-1h`,
query: ruleFields.ruleQuery,

View file

@ -37,8 +37,8 @@ describe('Non-default space rule detail page', { tags: ['@ess'] }, function () {
tags: ruleFields.ruleTags,
false_positives: ruleFields.falsePositives,
note: ruleFields.investigationGuide,
timeline_id: response.body.data.persistTimeline.timeline.savedObjectId,
timeline_title: response.body.data.persistTimeline.timeline.title ?? '',
timeline_id: response.body.savedObjectId,
timeline_title: response.body.title ?? '',
interval: ruleFields.ruleInterval,
from: `now-1h`,
query: ruleFields.ruleQuery,

View file

@ -28,7 +28,7 @@ describe('attach timeline to case', { tags: ['@ess', '@serverless'] }, () => {
deleteTimelines();
deleteCases();
createTimeline().then((response) => {
cy.wrap(response.body.data.persistTimeline.timeline).as('myTimeline');
cy.wrap(response.body).as('myTimeline');
});
});
@ -63,9 +63,7 @@ describe('attach timeline to case', { tags: ['@ess', '@serverless'] }, () => {
login();
deleteTimelines();
deleteCases();
createTimeline().then((response) =>
cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId')
);
createTimeline().then((response) => cy.wrap(response.body.savedObjectId).as('timelineId'));
createCase(getCase1()).then((response) => cy.wrap(response.body.id).as('caseId'));
});

View file

@ -67,7 +67,7 @@ describe('Cases', { tags: ['@ess', '@serverless'] }, () => {
...getCase1(),
timeline: {
...getCase1().timeline,
id: response.body.data.persistTimeline.timeline.savedObjectId,
id: response.body.savedObjectId,
},
})
.as('mycase')

View file

@ -52,7 +52,7 @@ describe('Overview Page', { tags: ['@ess', '@serverless'] }, () => {
describe('Favorite Timelines', { tags: ['@skipInServerless'] }, () => {
it('should appear on overview page', () => {
createTimeline()
.then((response) => response.body.data.persistTimeline.timeline.savedObjectId)
.then((response) => response.body.savedObjectId)
.then((timelineId: string) => {
favoriteTimeline({ timelineId, timelineType: 'default' }).then(() => {
visitWithTimeRange(OVERVIEW_URL);

View file

@ -304,7 +304,7 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => {
cy.wait('@timeline').then(({ response }) => {
closeTimeline();
cy.wrap(response?.statusCode).should('eql', 200);
const timelineId = response?.body.data.persistTimeline.timeline.savedObjectId;
const timelineId = response?.body.savedObjectId;
visitWithTimeRange('/app/home');
visitWithTimeRange(`/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`);
cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).should('exist');

View file

@ -52,7 +52,7 @@ describe('Ransomware Prevention Alerts', { tags: ['@ess', '@serverless'] }, () =
deleteTimelines();
login();
createTimeline({ ...getTimeline(), query: 'event.code: "ransomware"' }).then((response) => {
cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId');
cy.wrap(response.body.savedObjectId).as('timelineId');
});
});

View file

@ -100,11 +100,9 @@ describe('Timeline scope', { tags: ['@ess', '@serverless', '@skipInServerless']
beforeEach(() => {
login();
deleteTimelines();
createTimeline().then((response) =>
cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId')
);
createTimeline().then((response) => cy.wrap(response.body.savedObjectId).as('timelineId'));
createTimeline(getTimelineModifiedSourcerer()).then((response) =>
cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('auditbeatTimelineId')
cy.wrap(response.body.savedObjectId).as('auditbeatTimelineId')
);
visitWithTimeRange(TIMELINES_URL);
refreshUntilAlertsIndexExists();

View file

@ -83,7 +83,7 @@ describe.skip('Timeline Templates', { tags: ['@ess', '@serverless'] }, () => {
closeTimeline();
cy.wait('@timeline').then(({ response }) => {
const { createdBy, savedObjectId } = response?.body.data.persistTimeline.timeline;
const { createdBy, savedObjectId } = response?.body;
cy.log('Verify template shows on the table in the templates tab');
@ -122,7 +122,7 @@ describe.skip('Timeline Templates', { tags: ['@ess', '@serverless'] }, () => {
addNameToTimelineAndSave(savedName);
cy.wait('@timeline').then(({ response }) => {
const { createdBy, savedObjectId } = response?.body.data.persistTimeline.timeline;
const { createdBy, savedObjectId } = response?.body;
cy.log('Check that the template has been created correctly');

View file

@ -20,8 +20,8 @@ describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
deleteTimelines();
createTimelineTemplate().then((response) => {
cy.wrap(response).as('templateResponse');
cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('templateId');
cy.wrap(response.body.data.persistTimeline.timeline.title).as('templateTitle');
cy.wrap(response.body.savedObjectId).as('templateId');
cy.wrap(response.body.title).as('templateTitle');
});
});

View file

@ -26,7 +26,7 @@ describe('Correlation tab', { tags: ['@ess', '@serverless'] }, () => {
cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
createTimeline().then((response) => {
visit(TIMELINES_URL);
openTimeline(response.body.data.persistTimeline.timeline.savedObjectId);
openTimeline(response.body.savedObjectId);
addEqlToTimeline(eql);
saveTimeline();
cy.wait('@updateTimeline');

View file

@ -53,8 +53,7 @@ const INITIAL_END_DATE = 'Jan 19, 2024 @ 20:33:29.186';
const TIMELINE_REQ_WITH_SAVED_SEARCH = 'TIMELINE_REQ_WITH_SAVED_SEARCH';
const TIMELINE_PATCH_REQ = 'TIMELINE_PATCH_REQ';
const TIMELINE_RESPONSE_SAVED_OBJECT_ID_PATH =
'response.body.data.persistTimeline.timeline.savedObjectId';
const TIMELINE_RESPONSE_SAVED_OBJECT_ID_PATH = 'response.body.savedObjectId';
const esqlQuery = 'from auditbeat-* | where ecs.version == "8.0.0"';
const handleIntercepts = () => {

View file

@ -34,11 +34,11 @@ describe.skip('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
}).as('export');
createTimeline().then((response) => {
cy.wrap(response).as('timelineResponse1');
cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId1');
cy.wrap(response.body.savedObjectId).as('timelineId1');
});
createTimeline().then((response) => {
cy.wrap(response).as('timelineResponse2');
cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId2');
cy.wrap(response.body.savedObjectId).as('timelineId2');
});
visit(TIMELINES_URL);
});

View file

@ -33,7 +33,7 @@ describe('Timeline notes tab', { tags: ['@ess', '@serverless'] }, () => {
deleteTimelines();
createTimeline(getTimelineNonValidQuery())
.then((response) => response.body.data.persistTimeline.timeline.savedObjectId)
.then((response) => response.body.savedObjectId)
.then((timelineId: string) => {
login();
visitTimeline(timelineId);

View file

@ -35,7 +35,7 @@ describe('Open timeline modal', { tags: ['@serverless', '@ess'] }, () => {
login();
visit(TIMELINES_URL);
createTimeline()
.then((response) => response.body.data.persistTimeline.timeline.savedObjectId)
.then((response) => response.body.savedObjectId)
.then((timelineId: string) => {
refreshTimelinesUntilTimeLinePresent(timelineId)
// This cy.wait is here because we cannot do a pipe on a timeline as that will introduce multiple URL

View file

@ -77,9 +77,7 @@ describe('Row renderers', { tags: ['@ess', '@serverless'] }, () => {
addNameToTimelineAndSave('Test');
cy.wait('@excludedNetflow').then((interception) => {
expect(
interception?.response?.body.data.persistTimeline.timeline.excludedRowRendererIds
).to.contain('netflow');
expect(interception?.response?.body.excludedRowRendererIds).to.contain('netflow');
});
// open modal, filter and check
@ -93,9 +91,7 @@ describe('Row renderers', { tags: ['@ess', '@serverless'] }, () => {
saveTimeline();
cy.wait('@includedNetflow').then((interception) => {
expect(
interception?.response?.body.data.persistTimeline.timeline.excludedRowRendererIds
).not.to.contain('netflow');
expect(interception?.response?.body.excludedRowRendererIds).not.to.contain('netflow');
});
});

View file

@ -66,7 +66,7 @@ describe('Timeline search and filters', { tags: ['@ess', '@serverless'] }, () =>
addNameToTimelineAndSave('Test');
cy.wait('@update').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
cy.wrap(response?.body.data.persistTimeline.timeline.kqlMode).should('eql', 'filter');
cy.wrap(response?.body.kqlMode).should('eql', 'filter');
cy.get(ADD_FILTER).should('exist');
});
});
@ -76,7 +76,7 @@ describe('Timeline search and filters', { tags: ['@ess', '@serverless'] }, () =>
addNameToTimelineAndSave('Test');
cy.wait('@update').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
cy.wrap(response?.body.data.persistTimeline.timeline.kqlMode).should('eql', 'search');
cy.wrap(response?.body.kqlMode).should('eql', 'search');
cy.get(ADD_FILTER).should('not.exist');
});
});

View file

@ -40,7 +40,7 @@ describe.skip('timeline overview search', { tags: ['@ess', '@serverless'] }, ()
// create timeline and favorite it
// we're doing it through the UI because doing it through the API currently has a problem on MKI environment
createTimeline(mockFavoritedTimeline)
.then((response) => response.body.data.persistTimeline.timeline.savedObjectId)
.then((response) => response.body.savedObjectId)
.then((timelineId) => {
refreshTimelinesUntilTimeLinePresent(timelineId);
openTimelineById(timelineId);

View file

@ -24,8 +24,8 @@ describe('Open timeline', { tags: ['@serverless', '@ess'] }, () => {
deleteTimelines();
visit(TIMELINES_URL);
createTimeline().then((response) => {
timelineSavedObjectId = response.body.data.persistTimeline.timeline.savedObjectId;
return response.body.data.persistTimeline.timeline.savedObjectId;
timelineSavedObjectId = response.body.savedObjectId;
return response.body.savedObjectId;
});
createRule(getNewRule());
visitWithTimeRange(ALERTS_URL);

View file

@ -72,7 +72,7 @@ export const expectedExportedTimelineTemplate = (
templateResponse: Cypress.Response<PersistTimelineResponse>,
username: string
) => {
const timelineTemplateBody = templateResponse.body.data.persistTimeline.timeline;
const timelineTemplateBody = templateResponse.body;
return {
savedObjectId: timelineTemplateBody.savedObjectId,
@ -118,7 +118,7 @@ export const expectedExportedTimeline = (
timelineResponse: Cypress.Response<PersistTimelineResponse>,
username: string
) => {
const timelineBody = timelineResponse.body.data.persistTimeline.timeline;
const timelineBody = timelineResponse.body;
return {
savedObjectId: timelineBody.savedObjectId,

View file

@ -6,7 +6,6 @@
*/
import type {
DeleteTimelinesResponse,
GetTimelinesResponse,
PatchTimelineResponse,
} from '@kbn/security-solution-plugin/common/api/timeline';
@ -168,7 +167,7 @@ export const getAllTimelines = () =>
export const deleteTimelines = () => {
getAllTimelines().then(($timelines) => {
const savedObjectIds = $timelines.body.timeline.map((timeline) => timeline.savedObjectId);
rootRequest<DeleteTimelinesResponse>({
rootRequest({
method: 'DELETE',
url: 'api/timeline',
body: {

View file

@ -86,21 +86,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
);
await pageObjects.timeline.navigateToTimelineList();
await pageObjects.timeline.openTimelineById(
timeline.data.persistTimeline.timeline.savedObjectId
);
await pageObjects.timeline.openTimelineById(timeline.savedObjectId);
await pageObjects.timeline.setDateRange('Last 1 year');
await pageObjects.timeline.waitForEvents(60_000 * 2);
});
after(async () => {
if (timeline) {
log.info(
`Cleaning up created timeline [${timeline.data.persistTimeline.timeline.title} - ${timeline.data.persistTimeline.timeline.savedObjectId}]`
);
await timelineTestService.deleteTimeline(
timeline.data.persistTimeline.timeline.savedObjectId
);
log.info(`Cleaning up created timeline [${timeline.title} - ${timeline.savedObjectId}]`);
await timelineTestService.deleteTimeline(timeline.savedObjectId);
}
});

Some files were not shown because too many files have changed in this diff Show more