mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[8.x] [Attack Discovery][Scheduling] UI: "Attack Discovery Scheduling" management (#12007) (#217917) (#218267)
# Backport This will backport the following commits from `main` to `8.x`: - [[Attack Discovery][Scheduling] UI: "Attack Discovery Scheduling" management (#12007) (#217917)](https://github.com/elastic/kibana/pull/217917) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Ievgen Sorokopud","email":"ievgen.sorokopud@elastic.co"},"sourceCommit":{"committedDate":"2025-04-15T13:06:06Z","message":"[Attack Discovery][Scheduling] UI: \"Attack Discovery Scheduling\" management (#12007) (#217917)\n\n## Summary\n\nMain ticket ([Internal\nlink](https://github.com/elastic/security-team/issues/12007))\n\nThese changes add the attack discovery schedules management table.\n\n\nhttps://github.com/user-attachments/assets/619ad1d6-d919-4a8d-b743-6a73fbfbf318\n\n## Key changes\n\n* UI side API handlers\n* Create schedule workflow\n* Schedules table\n* Enable schedule from the table\n* Disable schedule from the table\n* Delete schedule from the table\n* Pagination and sorting in find schedules API\n\n## NOTES\n\nThe feature is hidden behind the feature flag (in `kibana.dev.yml`):\n\n```\nfeature_flags.overrides:\n securitySolution.assistantAttackDiscoverySchedulingEnabled: true\n```","sha":"10943319b29f8efddb4fa641cd150083de994f16","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team: SecuritySolution","Team:Security Generative AI","backport:version","v9.1.0","v8.19.0"],"title":"[Attack Discovery][Scheduling] UI: \"Attack Discovery Scheduling\" management (#12007)","number":217917,"url":"https://github.com/elastic/kibana/pull/217917","mergeCommit":{"message":"[Attack Discovery][Scheduling] UI: \"Attack Discovery Scheduling\" management (#12007) (#217917)\n\n## Summary\n\nMain ticket ([Internal\nlink](https://github.com/elastic/security-team/issues/12007))\n\nThese changes add the attack discovery schedules management table.\n\n\nhttps://github.com/user-attachments/assets/619ad1d6-d919-4a8d-b743-6a73fbfbf318\n\n## Key changes\n\n* UI side API handlers\n* Create schedule workflow\n* Schedules table\n* Enable schedule from the table\n* Disable schedule from the table\n* Delete schedule from the table\n* Pagination and sorting in find schedules API\n\n## NOTES\n\nThe feature is hidden behind the feature flag (in `kibana.dev.yml`):\n\n```\nfeature_flags.overrides:\n securitySolution.assistantAttackDiscoverySchedulingEnabled: true\n```","sha":"10943319b29f8efddb4fa641cd150083de994f16"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217917","number":217917,"mergeCommit":{"message":"[Attack Discovery][Scheduling] UI: \"Attack Discovery Scheduling\" management (#12007) (#217917)\n\n## Summary\n\nMain ticket ([Internal\nlink](https://github.com/elastic/security-team/issues/12007))\n\nThese changes add the attack discovery schedules management table.\n\n\nhttps://github.com/user-attachments/assets/619ad1d6-d919-4a8d-b743-6a73fbfbf318\n\n## Key changes\n\n* UI side API handlers\n* Create schedule workflow\n* Schedules table\n* Enable schedule from the table\n* Disable schedule from the table\n* Delete schedule from the table\n* Pagination and sorting in find schedules API\n\n## NOTES\n\nThe feature is hidden behind the feature flag (in `kibana.dev.yml`):\n\n```\nfeature_flags.overrides:\n securitySolution.assistantAttackDiscoverySchedulingEnabled: true\n```","sha":"10943319b29f8efddb4fa641cd150083de994f16"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: Ievgen Sorokopud <ievgen.sorokopud@elastic.co>
This commit is contained in:
parent
6099de25ac
commit
a657a38eb6
85 changed files with 3216 additions and 88 deletions
|
@ -75,11 +75,13 @@ const SecurityAttackDiscoveryAlertRequired = rt.type({
|
|||
'kibana.alert.attack_discovery.api_config': schemaUnknown,
|
||||
'kibana.alert.attack_discovery.details_markdown': schemaString,
|
||||
'kibana.alert.attack_discovery.details_markdown_with_replacements': schemaString,
|
||||
'kibana.alert.attack_discovery.replacements.uuid': schemaString,
|
||||
'kibana.alert.attack_discovery.replacements.value': schemaString,
|
||||
'kibana.alert.attack_discovery.summary_markdown': schemaString,
|
||||
'kibana.alert.attack_discovery.summary_markdown_with_replacements': schemaString,
|
||||
'kibana.alert.attack_discovery.title': schemaString,
|
||||
'kibana.alert.attack_discovery.title_with_replacements': schemaString,
|
||||
'kibana.alert.attack_discovery.users.id': schemaString,
|
||||
'kibana.alert.attack_discovery.users.name': schemaString,
|
||||
'kibana.alert.instance.id': schemaString,
|
||||
'kibana.alert.rule.category': schemaString,
|
||||
'kibana.alert.rule.consumer': schemaString,
|
||||
|
@ -105,9 +107,10 @@ const SecurityAttackDiscoveryAlertOptional = rt.partial({
|
|||
'kibana.alert.attack_discovery.mitre_attack_tactics': schemaStringArray,
|
||||
'kibana.alert.attack_discovery.replacements': schemaUnknown,
|
||||
'kibana.alert.attack_discovery.user.id': schemaString,
|
||||
'kibana.alert.attack_discovery.user.name': schemaString,
|
||||
'kibana.alert.attack_discovery.users': rt.array(
|
||||
rt.partial({
|
||||
name: schemaString,
|
||||
id: schemaString,
|
||||
})
|
||||
),
|
||||
'kibana.alert.case_ids': schemaStringArray,
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
export const ELASTIC_AI_ASSISTANT_URL = '/api/security_ai_assistant';
|
||||
export const ELASTIC_AI_ASSISTANT_INTERNAL_URL = '/internal/elastic_assistant';
|
||||
|
||||
export const POST_ACTIONS_CONNECTOR_EXECUTE =
|
||||
`${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/actions/connector/{connectorId}/_execute` as const;
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL =
|
||||
`${ELASTIC_AI_ASSISTANT_URL}/current_user/conversations` as const;
|
||||
export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID =
|
||||
|
@ -65,3 +68,15 @@ export const DEFEND_INSIGHTS_BY_ID = `${DEFEND_INSIGHTS}/{id}`;
|
|||
export const ATTACK_DISCOVERY_SCHEDULES_ENABLED_FEATURE_FLAG =
|
||||
'securitySolution.assistantAttackDiscoverySchedulingEnabled' as const;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID = 'attack-discovery' as const;
|
||||
|
||||
export const ATTACK_DISCOVERY = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/attack_discovery` as const;
|
||||
export const ATTACK_DISCOVERY_BY_CONNECTOR_ID = `${ATTACK_DISCOVERY}/{connectorId}` as const;
|
||||
export const ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID =
|
||||
`${ATTACK_DISCOVERY}/cancel/{connectorId}` as const;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES = `${ATTACK_DISCOVERY}/schedules` as const;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_BY_ID = `${ATTACK_DISCOVERY_SCHEDULES}/{id}` as const;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE =
|
||||
`${ATTACK_DISCOVERY_SCHEDULES}/{id}/_enable` as const;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE =
|
||||
`${ATTACK_DISCOVERY_SCHEDULES}/{id}/_disable` as const;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_FIND = `${ATTACK_DISCOVERY_SCHEDULES}/_find` as const;
|
||||
|
|
|
@ -16,14 +16,26 @@
|
|||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
import { NonEmptyString } from '../common_attributes.gen';
|
||||
import { AttackDiscoverySchedule } from './schedules.gen';
|
||||
|
||||
export type FindAttackDiscoverySchedulesRequestQuery = z.infer<
|
||||
typeof FindAttackDiscoverySchedulesRequestQuery
|
||||
>;
|
||||
export const FindAttackDiscoverySchedulesRequestQuery = z.object({
|
||||
page: z.coerce.number().optional(),
|
||||
perPage: z.coerce.number().optional(),
|
||||
sortField: NonEmptyString.optional(),
|
||||
sortDirection: z.enum(['asc', 'desc']).optional(),
|
||||
});
|
||||
export type FindAttackDiscoverySchedulesRequestQueryInput = z.input<
|
||||
typeof FindAttackDiscoverySchedulesRequestQuery
|
||||
>;
|
||||
|
||||
export type FindAttackDiscoverySchedulesResponse = z.infer<
|
||||
typeof FindAttackDiscoverySchedulesResponse
|
||||
>;
|
||||
export const FindAttackDiscoverySchedulesResponse = z.object({
|
||||
page: z.number(),
|
||||
perPage: z.number(),
|
||||
total: z.number(),
|
||||
data: z.array(AttackDiscoverySchedule),
|
||||
});
|
||||
|
|
|
@ -12,6 +12,30 @@ paths:
|
|||
summary: Finds attack discovery schedules
|
||||
tags:
|
||||
- attack_discovery_schedule
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
- name: perPage
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
- name: sortField
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
$ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
|
||||
- name: sortDirection
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
|
@ -25,10 +49,6 @@ paths:
|
|||
- total
|
||||
- data
|
||||
properties:
|
||||
page:
|
||||
type: number
|
||||
perPage:
|
||||
type: number
|
||||
total:
|
||||
type: number
|
||||
data:
|
||||
|
|
|
@ -31,7 +31,14 @@ export const AttackDiscoveryScheduleParams = z.object({
|
|||
/**
|
||||
* LLM API configuration.
|
||||
*/
|
||||
apiConfig: ApiConfig,
|
||||
apiConfig: ApiConfig.merge(
|
||||
z.object({
|
||||
/**
|
||||
* The name of the connector
|
||||
*/
|
||||
name: z.string(),
|
||||
})
|
||||
),
|
||||
end: z.string().optional(),
|
||||
filter: z.object({}).catchall(z.unknown()).optional(),
|
||||
size: z.number(),
|
||||
|
|
|
@ -72,7 +72,15 @@ components:
|
|||
type: string
|
||||
apiConfig:
|
||||
description: LLM API configuration.
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
|
||||
allOf:
|
||||
- $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
|
||||
- type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
description: The name of the connector
|
||||
type: string
|
||||
end:
|
||||
type: string
|
||||
filter:
|
||||
|
|
|
@ -10,18 +10,6 @@ export const PLUGIN_NAME = 'elasticAssistant';
|
|||
|
||||
export const BASE_PATH = '/internal/elastic_assistant';
|
||||
|
||||
export const POST_ACTIONS_CONNECTOR_EXECUTE = `${BASE_PATH}/actions/connector/{connectorId}/_execute`;
|
||||
|
||||
// Attack discovery
|
||||
export const ATTACK_DISCOVERY = `${BASE_PATH}/attack_discovery`;
|
||||
export const ATTACK_DISCOVERY_BY_CONNECTOR_ID = `${ATTACK_DISCOVERY}/{connectorId}`;
|
||||
export const ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID = `${ATTACK_DISCOVERY}/cancel/{connectorId}`;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES = `${ATTACK_DISCOVERY}/schedules`;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_BY_ID = `${ATTACK_DISCOVERY_SCHEDULES}/{id}`;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE = `${ATTACK_DISCOVERY_SCHEDULES}/{id}/_enable`;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE = `${ATTACK_DISCOVERY_SCHEDULES}/{id}/_disable`;
|
||||
export const ATTACK_DISCOVERY_SCHEDULES_FIND = `${ATTACK_DISCOVERY_SCHEDULES}/_find`;
|
||||
|
||||
export const CONVERSATIONS_TABLE_MAX_PAGE_SIZE = 100;
|
||||
export const ANONYMIZATION_FIELDS_TABLE_MAX_PAGE_SIZE = 100;
|
||||
export const PROMPTS_TABLE_MAX_PAGE_SIZE = 100;
|
||||
|
|
|
@ -27,6 +27,7 @@ export const getAttackDiscoveryCreateScheduleMock = (
|
|||
params: {
|
||||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
apiConfig: {
|
||||
name: 'Mock GPT-4o',
|
||||
connectorId: 'gpt-4o',
|
||||
actionTypeId: '.gen-ai',
|
||||
},
|
||||
|
@ -102,6 +103,7 @@ export const getAttackDiscoveryScheduleMock = (
|
|||
params: {
|
||||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
apiConfig: {
|
||||
name: 'Mock GPT-4o',
|
||||
connectorId: 'gpt-4o',
|
||||
actionTypeId: '.gen-ai',
|
||||
},
|
||||
|
|
|
@ -5,17 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { httpServerMock } from '@kbn/core/server/mocks';
|
||||
import {
|
||||
ATTACK_DISCOVERY,
|
||||
ATTACK_DISCOVERY_BY_CONNECTOR_ID,
|
||||
ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID,
|
||||
ATTACK_DISCOVERY_SCHEDULES,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_FIND,
|
||||
CAPABILITIES,
|
||||
} from '../../common/constants';
|
||||
import { CAPABILITIES } from '../../common/constants';
|
||||
import type {
|
||||
CreateAttackDiscoverySchedulesRequestBody,
|
||||
DefendInsightsGetRequestQuery,
|
||||
|
@ -26,6 +16,14 @@ import type {
|
|||
UpdateKnowledgeBaseEntryRequestParams,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
ATTACK_DISCOVERY,
|
||||
ATTACK_DISCOVERY_BY_CONNECTOR_ID,
|
||||
ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID,
|
||||
ATTACK_DISCOVERY_SCHEDULES,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_FIND,
|
||||
AttackDiscoveryPostRequestBody,
|
||||
ConversationCreateProps,
|
||||
ConversationUpdateProps,
|
||||
|
|
|
@ -24,12 +24,66 @@ describe('AttackDiscoveryScheduleDataClient', () => {
|
|||
});
|
||||
|
||||
describe('findSchedules', () => {
|
||||
it('should call `rulesClient.find` with the correct filter', async () => {
|
||||
it('should call `rulesClient.find` with the correct rule type', async () => {
|
||||
const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams);
|
||||
await scheduleDataClient.findSchedules();
|
||||
|
||||
expect(scheduleDataClientParams.rulesClient.find).toHaveBeenCalledWith({
|
||||
options: { filter: `alert.attributes.alertTypeId: attack-discovery` },
|
||||
options: {
|
||||
page: 1,
|
||||
ruleTypeIds: ['attack-discovery'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call `rulesClient.find` with the correct `page`', async () => {
|
||||
const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams);
|
||||
await scheduleDataClient.findSchedules({ page: 10 });
|
||||
|
||||
expect(scheduleDataClientParams.rulesClient.find).toHaveBeenCalledWith({
|
||||
options: {
|
||||
page: 11,
|
||||
ruleTypeIds: ['attack-discovery'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call `rulesClient.find` with the correct `perPage`', async () => {
|
||||
const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams);
|
||||
await scheduleDataClient.findSchedules({ perPage: 23 });
|
||||
|
||||
expect(scheduleDataClientParams.rulesClient.find).toHaveBeenCalledWith({
|
||||
options: {
|
||||
page: 1,
|
||||
perPage: 23,
|
||||
ruleTypeIds: ['attack-discovery'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call `rulesClient.find` with the correct `sortField`', async () => {
|
||||
const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams);
|
||||
await scheduleDataClient.findSchedules({ sort: { sortField: 'name' } });
|
||||
|
||||
expect(scheduleDataClientParams.rulesClient.find).toHaveBeenCalledWith({
|
||||
options: {
|
||||
page: 1,
|
||||
sortField: 'name',
|
||||
ruleTypeIds: ['attack-discovery'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call `rulesClient.find` with the correct `sortDirection`', async () => {
|
||||
const scheduleDataClient = new AttackDiscoveryScheduleDataClient(scheduleDataClientParams);
|
||||
await scheduleDataClient.findSchedules({ sort: { sortDirection: 'desc' } });
|
||||
|
||||
expect(scheduleDataClientParams.rulesClient.find).toHaveBeenCalledWith({
|
||||
options: {
|
||||
page: 1,
|
||||
sortOrder: 'desc',
|
||||
ruleTypeIds: ['attack-discovery'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
|
||||
AttackDiscoveryScheduleParams,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { AttackDiscoveryScheduleFindOptions } from '../types';
|
||||
|
||||
/**
|
||||
* Params for when creating AttackDiscoveryScheduleDataClient in Request Context Factory. Useful if needing to modify
|
||||
|
@ -28,13 +29,19 @@ export interface AttackDiscoveryScheduleDataClientParams {
|
|||
export class AttackDiscoveryScheduleDataClient {
|
||||
constructor(public readonly options: AttackDiscoveryScheduleDataClientParams) {}
|
||||
|
||||
public findSchedules = async () => {
|
||||
public findSchedules = async ({
|
||||
page = 0,
|
||||
perPage,
|
||||
sort: sortParam = {},
|
||||
}: AttackDiscoveryScheduleFindOptions = {}) => {
|
||||
// TODO: add filtering
|
||||
// TODO: add sorting
|
||||
// TODO: add pagination
|
||||
const rules = await this.options.rulesClient.find<AttackDiscoveryScheduleParams>({
|
||||
options: {
|
||||
filter: `alert.attributes.alertTypeId: ${ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID}`,
|
||||
page: page + 1,
|
||||
perPage,
|
||||
sortField: sortParam.sortField,
|
||||
sortOrder: sortParam.sortDirection,
|
||||
ruleTypeIds: [ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID],
|
||||
},
|
||||
});
|
||||
return rules;
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
ALERT_ATTACK_DISCOVERY_USERS_ID,
|
||||
ALERT_ATTACK_DISCOVERY_USERS_NAME,
|
||||
ALERT_ATTACK_DISCOVERY_USER_ID,
|
||||
ALERT_ATTACK_DISCOVERY_USER_NAME,
|
||||
ALERT_RISK_SCORE,
|
||||
} from './field_names';
|
||||
|
||||
|
@ -128,12 +129,12 @@ export const attackDiscoveryAlertFieldMap: FieldMap = {
|
|||
[ALERT_ATTACK_DISCOVERY_REPLACEMENTS_VALUE]: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
[ALERT_ATTACK_DISCOVERY_REPLACEMENTS_UUID]: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
[ALERT_ATTACK_DISCOVERY_SUMMARY_MARKDOWN]: {
|
||||
type: 'text',
|
||||
|
@ -163,6 +164,13 @@ export const attackDiscoveryAlertFieldMap: FieldMap = {
|
|||
array: false,
|
||||
required: false,
|
||||
},
|
||||
[ALERT_ATTACK_DISCOVERY_USER_NAME]: {
|
||||
// optional field for ad hock attack discoveries
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
|
||||
[ALERT_ATTACK_DISCOVERY_USERS]: {
|
||||
type: 'nested',
|
||||
array: true,
|
||||
|
@ -171,11 +179,11 @@ export const attackDiscoveryAlertFieldMap: FieldMap = {
|
|||
[ALERT_ATTACK_DISCOVERY_USERS_ID]: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
[ALERT_ATTACK_DISCOVERY_USERS_NAME]: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -30,6 +30,7 @@ export const ALERT_ATTACK_DISCOVERY_TITLE = `${ALERT_ATTACK_DISCOVERY}.title` as
|
|||
export const ALERT_ATTACK_DISCOVERY_TITLE_WITH_REPLACEMENTS =
|
||||
`${ALERT_ATTACK_DISCOVERY}.title_with_replacements` as const;
|
||||
export const ALERT_ATTACK_DISCOVERY_USER_ID = `${ALERT_ATTACK_DISCOVERY}.user.id` as const;
|
||||
export const ALERT_ATTACK_DISCOVERY_USER_NAME = `${ALERT_ATTACK_DISCOVERY}.user.name` as const;
|
||||
|
||||
// Alert base fields
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import { RuleExecutorOptions, RuleType, RuleTypeState } from '@kbn/alerting-plugin/server';
|
||||
import { SecurityAttackDiscoveryAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { AttackDiscoveryScheduleParams } from '@kbn/elastic-assistant-common';
|
||||
|
@ -28,3 +29,14 @@ export type AttackDiscoveryScheduleType = RuleType<
|
|||
never,
|
||||
SecurityAttackDiscoveryAlert
|
||||
>;
|
||||
|
||||
export interface AttackDiscoveryScheduleSort {
|
||||
sortDirection?: estypes.SortOrder;
|
||||
sortField?: string;
|
||||
}
|
||||
|
||||
export interface AttackDiscoveryScheduleFindOptions {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sort?: AttackDiscoveryScheduleSort;
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ import {
|
|||
AttackDiscoveryGetResponse,
|
||||
API_VERSIONS,
|
||||
AttackDiscoveryGetRequestParams,
|
||||
ATTACK_DISCOVERY_BY_CONNECTOR_ID,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { updateAttackDiscoveryLastViewedAt, getAttackDiscoveryStats } from '../helpers/helpers';
|
||||
import { ATTACK_DISCOVERY_BY_CONNECTOR_ID } from '../../../../common/constants';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@ import {
|
|||
AttackDiscoveryCancelResponse,
|
||||
API_VERSIONS,
|
||||
AttackDiscoveryCancelRequestParams,
|
||||
ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { updateAttackDiscoveryStatusToCanceled } from '../../helpers/helpers';
|
||||
import { ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID } from '../../../../../common/constants';
|
||||
import { buildResponse } from '../../../../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../../types';
|
||||
|
||||
|
|
|
@ -10,11 +10,11 @@ import {
|
|||
AttackDiscoveryPostRequestBody,
|
||||
AttackDiscoveryPostResponse,
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { ATTACK_DISCOVERY } from '../../../../common/constants';
|
||||
import { updateAttackDiscoveryStatusToRunning } from '../helpers/helpers';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
|
|
|
@ -34,6 +34,7 @@ const mockApiConfig = {
|
|||
connectorId: 'connector-id',
|
||||
actionTypeId: '.bedrock',
|
||||
model: 'model',
|
||||
name: 'Test Bedrock',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
const mockRequestBody: CreateAttackDiscoverySchedulesRequestBody = {
|
||||
|
|
|
@ -10,13 +10,13 @@ import { transformError } from '@kbn/securitysolution-es-utils';
|
|||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY_SCHEDULES,
|
||||
ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
|
||||
CreateAttackDiscoverySchedulesRequestBody,
|
||||
CreateAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES } from '../../../../common/constants';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule';
|
||||
import { performChecks } from '../../helpers';
|
||||
|
|
|
@ -11,11 +11,11 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
|||
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
DeleteAttackDiscoverySchedulesRequestParams,
|
||||
DeleteAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '../../../../common/constants';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { performChecks } from '../../helpers';
|
||||
import { isFeatureAvailable } from './utils/is_feature_available';
|
||||
|
|
|
@ -11,11 +11,11 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
|||
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE,
|
||||
DisableAttackDiscoverySchedulesRequestParams,
|
||||
DisableAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE } from '../../../../common/constants';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { performChecks } from '../../helpers';
|
||||
import { isFeatureAvailable } from './utils/is_feature_available';
|
||||
|
|
|
@ -11,11 +11,11 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
|||
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE,
|
||||
EnableAttackDiscoverySchedulesRequestParams,
|
||||
EnableAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE } from '../../../../common/constants';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { performChecks } from '../../helpers';
|
||||
import { isFeatureAvailable } from './utils/is_feature_available';
|
||||
|
|
|
@ -36,6 +36,7 @@ const mockApiConfig = {
|
|||
connectorId: 'connector-id',
|
||||
actionTypeId: '.bedrock',
|
||||
model: 'model',
|
||||
name: 'Test Bedrock',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
const basicAttackDiscoveryScheduleMock = {
|
||||
|
@ -75,8 +76,6 @@ describe('findAttackDiscoverySchedulesRoute', () => {
|
|||
);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
data: [expect.objectContaining(basicAttackDiscoveryScheduleMock)],
|
||||
});
|
||||
|
|
|
@ -9,9 +9,13 @@ import type { IKibanaResponse, IRouter, Logger } from '@kbn/core/server';
|
|||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
|
||||
import { API_VERSIONS, FindAttackDiscoverySchedulesResponse } from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY_SCHEDULES_FIND,
|
||||
FindAttackDiscoverySchedulesRequestQuery,
|
||||
FindAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_FIND } from '../../../../common/constants';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule';
|
||||
import { performChecks } from '../../helpers';
|
||||
|
@ -34,6 +38,9 @@ export const findAttackDiscoverySchedulesRoute = (
|
|||
{
|
||||
version: API_VERSIONS.internal.v1,
|
||||
validate: {
|
||||
request: {
|
||||
query: buildRouteValidationWithZod(FindAttackDiscoverySchedulesRequestQuery),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: {
|
||||
|
@ -78,12 +85,18 @@ export const findAttackDiscoverySchedulesRoute = (
|
|||
});
|
||||
}
|
||||
|
||||
const results = await dataClient.findSchedules();
|
||||
const { page, perPage, total, data } = results;
|
||||
const { page, perPage, sortField, sortDirection } = request.query;
|
||||
|
||||
const results = await dataClient.findSchedules({
|
||||
page,
|
||||
perPage,
|
||||
sort: { sortField, sortDirection },
|
||||
});
|
||||
const { total, data } = results;
|
||||
|
||||
const schedules = data.map(convertAlertingRuleToSchedule);
|
||||
|
||||
return response.ok({ body: { page, perPage, total, data: schedules } });
|
||||
return response.ok({ body: { total, data: schedules } });
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
const error = transformError(err);
|
||||
|
|
|
@ -33,6 +33,7 @@ const mockApiConfig = {
|
|||
connectorId: 'connector-id',
|
||||
actionTypeId: '.bedrock',
|
||||
model: 'model',
|
||||
name: 'Test Bedrock',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
const basicAttackDiscoveryScheduleMock = {
|
||||
|
|
|
@ -11,11 +11,11 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
|||
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
GetAttackDiscoverySchedulesRequestParams,
|
||||
GetAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '../../../../common/constants';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule';
|
||||
import { performChecks } from '../../helpers';
|
||||
|
|
|
@ -34,6 +34,7 @@ const mockApiConfig = {
|
|||
connectorId: 'connector-id',
|
||||
actionTypeId: '.bedrock',
|
||||
model: 'model',
|
||||
name: 'Test Bedrock',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
const mockRequestBody: UpdateAttackDiscoverySchedulesRequestBody = {
|
||||
|
|
|
@ -11,12 +11,12 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
|||
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
UpdateAttackDiscoverySchedulesRequestBody,
|
||||
UpdateAttackDiscoverySchedulesRequestParams,
|
||||
UpdateAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildResponse } from '../../../lib/build_response';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '../../../../common/constants';
|
||||
import { ElasticAssistantRequestHandlerContext } from '../../../types';
|
||||
import { convertAlertingRuleToSchedule } from './utils/convert_alerting_rule_to_schedule';
|
||||
import { performChecks } from '../../helpers';
|
||||
|
|
|
@ -14,6 +14,7 @@ const mockApiConfig = {
|
|||
connectorId: 'connector-id',
|
||||
actionTypeId: '.bedrock',
|
||||
model: 'model',
|
||||
name: 'Test Bedrock',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
const basicAttackDiscoveryScheduleMock = {
|
||||
|
|
|
@ -18,6 +18,7 @@ const mockApiConfig = {
|
|||
connectorId: 'connector-id',
|
||||
actionTypeId: '.bedrock',
|
||||
model: 'model',
|
||||
name: 'Test Bedrock',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
const basicAttackDiscoveryScheduleMock = {
|
||||
|
|
|
@ -18,10 +18,10 @@ import {
|
|||
Replacements,
|
||||
pruneContentReferences,
|
||||
ExecuteConnectorRequestQuery,
|
||||
POST_ACTIONS_CONNECTOR_EXECUTE,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
|
||||
import { INVOKE_ASSISTANT_ERROR_EVENT } from '../lib/telemetry/event_based_telemetry';
|
||||
import { POST_ACTIONS_CONNECTOR_EXECUTE } from '../../common/constants';
|
||||
import { buildResponse } from '../lib/build_response';
|
||||
import { ElasticAssistantRequestHandlerContext, GetElser } from '../types';
|
||||
import {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AttackDiscoverySchedule,
|
||||
AttackDiscoveryScheduleCreateProps,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
export const mockAttackDiscoverySchedule: AttackDiscoverySchedule = {
|
||||
id: 'ffaa0a8a-3c35-4166-9f73-70baac2b6b42',
|
||||
name: 'Schedule - Sonnet 3.7 (Bedrock)',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: '2025-04-09T08:51:04.697Z',
|
||||
updatedAt: '2025-04-09T21:10:16.483Z',
|
||||
enabled: true,
|
||||
params: {
|
||||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
apiConfig: {
|
||||
connectorId: 'sonnet-3-7',
|
||||
actionTypeId: '.bedrock',
|
||||
name: 'Sonnet 3.7',
|
||||
},
|
||||
end: 'now',
|
||||
size: 100,
|
||||
start: 'now-24h',
|
||||
},
|
||||
schedule: {
|
||||
interval: '10m',
|
||||
},
|
||||
actions: [],
|
||||
lastExecution: {
|
||||
date: '2025-04-09T21:01:08.276Z',
|
||||
status: 'ok',
|
||||
duration: 26,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockCreateAttackDiscoverySchedule: AttackDiscoveryScheduleCreateProps = {
|
||||
name: 'Schedule - Sonnet 3.7 (Bedrock)',
|
||||
enabled: true,
|
||||
params: {
|
||||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
apiConfig: {
|
||||
connectorId: 'sonnet-3-7',
|
||||
actionTypeId: '.bedrock',
|
||||
name: 'Sonnet 3.7',
|
||||
},
|
||||
end: 'now',
|
||||
size: 100,
|
||||
start: 'now-24h',
|
||||
},
|
||||
schedule: {
|
||||
interval: '10m',
|
||||
},
|
||||
actions: [],
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
export const mockFindAttackDiscoverySchedules: {
|
||||
schedules: AttackDiscoverySchedule[];
|
||||
total: number;
|
||||
} = {
|
||||
total: 2,
|
||||
schedules: [
|
||||
{
|
||||
id: 'ffaa0a8a-3c35-4166-9f73-70baac2b6b42',
|
||||
name: 'Schedule - Sonnet 3.7 (Bedrock)',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: '2025-04-09T08:51:04.697Z',
|
||||
updatedAt: '2025-04-09T21:10:16.483Z',
|
||||
enabled: true,
|
||||
params: {
|
||||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
apiConfig: {
|
||||
connectorId: 'sonnet-3-7',
|
||||
actionTypeId: '.bedrock',
|
||||
name: 'Sonnet 3.7',
|
||||
},
|
||||
end: 'now',
|
||||
size: 100,
|
||||
start: 'now-24h',
|
||||
},
|
||||
schedule: {
|
||||
interval: '10m',
|
||||
},
|
||||
actions: [],
|
||||
lastExecution: {
|
||||
date: '2025-04-09T21:01:08.276Z',
|
||||
status: 'ok',
|
||||
duration: 26,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6826fb6f-de83-4e19-b9e4-15718bda02e6',
|
||||
name: 'Schedule 2',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: '2025-04-09T08:51:04.697Z',
|
||||
updatedAt: '2025-04-09T21:10:16.483Z',
|
||||
enabled: false,
|
||||
params: {
|
||||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
apiConfig: {
|
||||
connectorId: 'gpt-4o',
|
||||
actionTypeId: '.gen-ai',
|
||||
name: 'Test GPT-4o',
|
||||
},
|
||||
end: 'now',
|
||||
size: 120,
|
||||
start: 'now-24h',
|
||||
},
|
||||
schedule: {
|
||||
interval: '30m',
|
||||
},
|
||||
actions: [],
|
||||
lastExecution: {
|
||||
date: '2025-04-09T21:01:08.276Z',
|
||||
status: 'ok',
|
||||
duration: 26,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -34,3 +34,10 @@ export const SCHEDULE_TAB_LABEL = i18n.translate(
|
|||
defaultMessage: 'Schedule',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_NEW_SCHEDULE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.createNewScheduleButton',
|
||||
{
|
||||
defaultMessage: 'Create new schedule',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useScheduleView } from './use_schedule_view';
|
||||
import { useFindAttackDiscoverySchedules } from '../schedule/logic/use_find_schedules';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { useSourcererDataView } from '../../../../sourcerer/containers';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { mockFindAttackDiscoverySchedules } from '../../mock/mock_find_attack_discovery_schedules';
|
||||
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
matchPath: jest.fn(),
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
search: '',
|
||||
}),
|
||||
withRouter: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../sourcerer/containers');
|
||||
jest.mock('../schedule/logic/use_find_schedules');
|
||||
|
||||
const mockUseKibana = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
const mockUseSourcererDataView = useSourcererDataView as jest.MockedFunction<
|
||||
typeof useSourcererDataView
|
||||
>;
|
||||
const mockUseFindAttackDiscoverySchedules = useFindAttackDiscoverySchedules as jest.MockedFunction<
|
||||
typeof useFindAttackDiscoverySchedules
|
||||
>;
|
||||
|
||||
describe('useScheduleView', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseKibana.mockReturnValue({
|
||||
services: {
|
||||
lens: {
|
||||
EmbeddableComponent: () => <div data-test-subj="mockEmbeddableComponent" />,
|
||||
},
|
||||
triggersActionsUi: {
|
||||
...triggersActionsUiMock.createStart(),
|
||||
},
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
unifiedSearch: {
|
||||
ui: {
|
||||
SearchBar: () => <div data-test-subj="mockSearchBar" />,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useKibana>>);
|
||||
|
||||
mockUseSourcererDataView.mockReturnValue({
|
||||
sourcererDataView: {},
|
||||
loading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useSourcererDataView>>);
|
||||
|
||||
mockUseFindAttackDiscoverySchedules.mockReturnValue({
|
||||
data: mockFindAttackDiscoverySchedules,
|
||||
isLoading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useFindAttackDiscoverySchedules>>);
|
||||
});
|
||||
|
||||
it('should return the `empty schedules` page if there are no existing schedules', () => {
|
||||
mockUseFindAttackDiscoverySchedules.mockReturnValue({
|
||||
data: { schedules: [], total: 0 },
|
||||
isLoading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useFindAttackDiscoverySchedules>>);
|
||||
|
||||
const { result } = renderHook(() => useScheduleView());
|
||||
|
||||
render(<TestProviders>{result.current.scheduleView}</TestProviders>);
|
||||
|
||||
expect(screen.getByTestId('emptySchedule')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not return `create new schedule` action button if there are no existing schedules', () => {
|
||||
mockUseFindAttackDiscoverySchedules.mockReturnValue({
|
||||
data: { schedules: [], total: 0 },
|
||||
isLoading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useFindAttackDiscoverySchedules>>);
|
||||
|
||||
const { result } = renderHook(() => useScheduleView());
|
||||
|
||||
render(<TestProviders>{result.current.actionButtons}</TestProviders>);
|
||||
|
||||
expect(screen.queryByTestId('createNewSchedule')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return the `attack discovery schedules table` if there are existing schedules', () => {
|
||||
const { result } = renderHook(() => useScheduleView());
|
||||
|
||||
render(<TestProviders>{result.current.scheduleView}</TestProviders>);
|
||||
|
||||
expect(screen.getByTestId('schedulesTable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return `create new schedule` action button if there are existing schedules', () => {
|
||||
const { result } = renderHook(() => useScheduleView());
|
||||
|
||||
render(<TestProviders>{result.current.actionButtons}</TestProviders>);
|
||||
|
||||
expect(screen.getByTestId('createNewSchedule')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show create schedule flyout on `create new schedule` action button click', async () => {
|
||||
const { result } = renderHook(() => useScheduleView());
|
||||
|
||||
render(<TestProviders>{result.current.actionButtons}</TestProviders>);
|
||||
|
||||
expect(screen.getByTestId('createNewSchedule')).toBeInTheDocument();
|
||||
|
||||
const createNewSchedule = screen.getByTestId('createNewSchedule');
|
||||
act(() => {
|
||||
fireEvent.click(createNewSchedule);
|
||||
});
|
||||
|
||||
render(<TestProviders>{result.current.scheduleView}</TestProviders>);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('scheduleCreateFlyout')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSkeletonLoading,
|
||||
EuiSkeletonText,
|
||||
EuiSkeletonTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { useFindAttackDiscoverySchedules } from '../schedule/logic/use_find_schedules';
|
||||
import { EmptyPage } from '../schedule/empty_page';
|
||||
import { SchedulesTable } from '../schedule/schedules_table';
|
||||
import { CreateFlyout } from '../schedule/create_flyout';
|
||||
|
||||
export interface UseScheduleView {
|
||||
scheduleView: React.ReactNode;
|
||||
actionButtons: React.ReactNode;
|
||||
}
|
||||
|
||||
export const useScheduleView = (): UseScheduleView => {
|
||||
// showing / hiding the flyout:
|
||||
const [showFlyout, setShowFlyout] = useState<boolean>(false);
|
||||
const openFlyout = useCallback(() => setShowFlyout(true), []);
|
||||
const onClose = useCallback(() => setShowFlyout(false), []);
|
||||
|
||||
// TODO: add separate hook to fetch schedules stats/count
|
||||
const { data: { total } = { total: 0 }, isLoading: isDataLoading } =
|
||||
useFindAttackDiscoverySchedules();
|
||||
|
||||
const scheduleView = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<EuiSkeletonLoading
|
||||
isLoading={isDataLoading}
|
||||
loadingContent={
|
||||
<>
|
||||
<EuiSkeletonTitle />
|
||||
<EuiSkeletonText />
|
||||
</>
|
||||
}
|
||||
loadedContent={!total ? <EmptyPage /> : <SchedulesTable />}
|
||||
/>
|
||||
{showFlyout && <CreateFlyout onClose={onClose} />}
|
||||
</>
|
||||
);
|
||||
}, [isDataLoading, onClose, showFlyout, total]);
|
||||
|
||||
const actionButtons = useMemo(() => {
|
||||
return total ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="createNewSchedule"
|
||||
fill
|
||||
onClick={openFlyout}
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
{i18n.CREATE_NEW_SCHEDULE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null;
|
||||
}, [openFlyout, total]);
|
||||
|
||||
return { scheduleView, actionButtons };
|
||||
};
|
|
@ -12,6 +12,8 @@ import { useKibana } from '../../../../common/lib/kibana';
|
|||
import { useSourcererDataView } from '../../../../sourcerer/containers';
|
||||
import { useTabsView } from './use_tabs_view';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useFindAttackDiscoverySchedules } from '../schedule/logic/use_find_schedules';
|
||||
import { mockFindAttackDiscoverySchedules } from '../../mock/mock_find_attack_discovery_schedules';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
matchPath: jest.fn(),
|
||||
|
@ -22,6 +24,7 @@ jest.mock('react-router', () => ({
|
|||
}));
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../sourcerer/containers');
|
||||
jest.mock('../schedule/logic/use_find_schedules');
|
||||
|
||||
const defaultProps = {
|
||||
onSettingsReset: jest.fn(),
|
||||
|
@ -40,6 +43,9 @@ const mockUseKibana = useKibana as jest.MockedFunction<typeof useKibana>;
|
|||
const mockUseSourcererDataView = useSourcererDataView as jest.MockedFunction<
|
||||
typeof useSourcererDataView
|
||||
>;
|
||||
const mockUseFindAttackDiscoverySchedules = useFindAttackDiscoverySchedules as jest.MockedFunction<
|
||||
typeof useFindAttackDiscoverySchedules
|
||||
>;
|
||||
|
||||
describe('useTabsView', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -65,6 +71,11 @@ describe('useTabsView', () => {
|
|||
sourcererDataView: {},
|
||||
loading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useSourcererDataView>>);
|
||||
|
||||
mockUseFindAttackDiscoverySchedules.mockReturnValue({
|
||||
data: { schedules: [], total: 0 },
|
||||
isLoading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useFindAttackDiscoverySchedules>>);
|
||||
});
|
||||
|
||||
it('should return the alert selection component with `AlertSelectionQuery` when settings tab is selected', () => {
|
||||
|
@ -140,7 +151,29 @@ describe('useTabsView', () => {
|
|||
});
|
||||
render(<TestProviders>{result.current.tabsContainer}</TestProviders>);
|
||||
await waitFor(() => {
|
||||
expect(result.current.actionButtons).toBeUndefined();
|
||||
expect(result.current.actionButtons).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return `create new schedule` action button when schedule tab is selected and there are existing schedules', async () => {
|
||||
mockUseFindAttackDiscoverySchedules.mockReturnValue({
|
||||
data: mockFindAttackDiscoverySchedules,
|
||||
isLoading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useFindAttackDiscoverySchedules>>);
|
||||
|
||||
const { result } = renderHook(() => useTabsView(defaultProps));
|
||||
|
||||
render(<TestProviders>{result.current.tabsContainer}</TestProviders>);
|
||||
|
||||
const scheduleTabButton = screen.getByRole('tab', { name: 'Schedule' });
|
||||
act(() => {
|
||||
fireEvent.click(scheduleTabButton); // clicking invokes tab switching
|
||||
});
|
||||
|
||||
// Render action buttons of the Schedule tab
|
||||
render(<TestProviders>{result.current.actionButtons}</TestProviders>);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('createNewSchedule')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import * as i18n from './translations';
|
||||
import { useSettingsView } from './use_settings_view';
|
||||
import type { AlertsSelectionSettings } from '../types';
|
||||
import { Schedule } from '../schedule';
|
||||
import { useScheduleView } from './use_schedule_view';
|
||||
|
||||
/*
|
||||
* Fixes tabs to the top and allows the content to scroll.
|
||||
|
@ -54,6 +54,7 @@ export const useTabsView = ({
|
|||
onSettingsChanged,
|
||||
settings,
|
||||
});
|
||||
const { scheduleView, actionButtons: scheduleTabButtons } = useScheduleView();
|
||||
|
||||
const settingsTab: EuiTabbedContentTab = useMemo(
|
||||
() => ({
|
||||
|
@ -76,11 +77,11 @@ export const useTabsView = ({
|
|||
content: (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<Schedule />
|
||||
{scheduleView}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[]
|
||||
[scheduleView]
|
||||
);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
|
@ -112,8 +113,8 @@ export const useTabsView = ({
|
|||
}, [selectedTab, tabs]);
|
||||
|
||||
const actionButtons = useMemo(
|
||||
() => (selectedTabId === 'settings' ? filterActionButtons : undefined),
|
||||
[filterActionButtons, selectedTabId]
|
||||
() => (selectedTabId === 'settings' ? filterActionButtons : scheduleTabButtons),
|
||||
[filterActionButtons, scheduleTabButtons, selectedTabId]
|
||||
);
|
||||
|
||||
return { tabsContainer, actionButtons };
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { replaceParams } from '@kbn/openapi-common/shared';
|
||||
import type {
|
||||
AttackDiscoveryScheduleCreateProps,
|
||||
AttackDiscoveryScheduleUpdateProps,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
ATTACK_DISCOVERY_SCHEDULES,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_FIND,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
import {
|
||||
createAttackDiscoverySchedule,
|
||||
deleteAttackDiscoverySchedule,
|
||||
disableAttackDiscoverySchedule,
|
||||
enableAttackDiscoverySchedule,
|
||||
findAttackDiscoverySchedule,
|
||||
getAttackDiscoverySchedule,
|
||||
updateAttackDiscoverySchedule,
|
||||
} from '.';
|
||||
import { KibanaServices } from '../../../../../common/lib/kibana';
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
const mockKibanaServices = KibanaServices.get as jest.Mock;
|
||||
|
||||
describe('Schedule API', () => {
|
||||
beforeEach(() => {
|
||||
mockKibanaServices.mockReturnValue(coreMock.createStart({ basePath: '/mock' }));
|
||||
});
|
||||
|
||||
it('should send a create schedule POST request', async () => {
|
||||
const body = { test: 'test schedule' } as unknown as AttackDiscoveryScheduleCreateProps;
|
||||
await createAttackDiscoverySchedule({ body });
|
||||
|
||||
expect(mockKibanaServices().http.post).toHaveBeenCalledWith(ATTACK_DISCOVERY_SCHEDULES, {
|
||||
body: JSON.stringify(body),
|
||||
version: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send a fetch schedule GET request', async () => {
|
||||
const id = 'test-id-1';
|
||||
await getAttackDiscoverySchedule({ id });
|
||||
|
||||
expect(mockKibanaServices().http.get).toHaveBeenCalledWith(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id }),
|
||||
{ version: '1' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an update schedule PUT request', async () => {
|
||||
const id = 'test-id-2';
|
||||
const body = { test: 'test schedule' } as unknown as AttackDiscoveryScheduleUpdateProps;
|
||||
await updateAttackDiscoverySchedule({ id, body });
|
||||
|
||||
expect(mockKibanaServices().http.put).toHaveBeenCalledWith(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id }),
|
||||
{ body: JSON.stringify(body), version: '1' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send a delete schedule DELETE request', async () => {
|
||||
const id = 'test-id-3';
|
||||
await deleteAttackDiscoverySchedule({ id });
|
||||
|
||||
expect(mockKibanaServices().http.delete).toHaveBeenCalledWith(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id }),
|
||||
{ version: '1' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an enable schedule POST request', async () => {
|
||||
const id = 'test-id-4';
|
||||
await enableAttackDiscoverySchedule({ id });
|
||||
|
||||
expect(mockKibanaServices().http.post).toHaveBeenCalledWith(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE, { id }),
|
||||
{ version: '1' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send a disable schedule POST request', async () => {
|
||||
const id = 'test-id-5';
|
||||
await disableAttackDiscoverySchedule({ id });
|
||||
|
||||
expect(mockKibanaServices().http.post).toHaveBeenCalledWith(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE, { id }),
|
||||
{ version: '1' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send a find schedules GET request', async () => {
|
||||
const params = {
|
||||
page: 10,
|
||||
perPage: 123,
|
||||
sortField: 'test-field-1',
|
||||
sortDirection: 'desc' as 'asc' | 'desc',
|
||||
};
|
||||
await findAttackDiscoverySchedule(params);
|
||||
|
||||
expect(mockKibanaServices().http.get).toHaveBeenCalledWith(ATTACK_DISCOVERY_SCHEDULES_FIND, {
|
||||
version: '1',
|
||||
query: { ...params },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { replaceParams } from '@kbn/openapi-common/shared';
|
||||
|
||||
import type {
|
||||
AttackDiscoveryScheduleCreateProps,
|
||||
AttackDiscoveryScheduleUpdateProps,
|
||||
CreateAttackDiscoverySchedulesResponse,
|
||||
DeleteAttackDiscoverySchedulesResponse,
|
||||
DisableAttackDiscoverySchedulesResponse,
|
||||
EnableAttackDiscoverySchedulesResponse,
|
||||
FindAttackDiscoverySchedulesResponse,
|
||||
GetAttackDiscoverySchedulesResponse,
|
||||
UpdateAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
ATTACK_DISCOVERY_SCHEDULES,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE,
|
||||
ATTACK_DISCOVERY_SCHEDULES_FIND,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { KibanaServices } from '../../../../../common/lib/kibana';
|
||||
|
||||
export interface CreateAttackDiscoveryScheduleParams {
|
||||
/** The body containing the schedule attributes */
|
||||
body: AttackDiscoveryScheduleCreateProps;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Creates a new attack discovery schedule with the provided attributes. */
|
||||
export const createAttackDiscoverySchedule = async ({
|
||||
body,
|
||||
signal,
|
||||
}: CreateAttackDiscoveryScheduleParams): Promise<CreateAttackDiscoverySchedulesResponse> => {
|
||||
return KibanaServices.get().http.post<CreateAttackDiscoverySchedulesResponse>(
|
||||
ATTACK_DISCOVERY_SCHEDULES,
|
||||
{ body: JSON.stringify(body), version: '1', signal }
|
||||
);
|
||||
};
|
||||
|
||||
export interface GetAttackDiscoveryScheduleParams {
|
||||
/** `id` of the attack discovery schedule */
|
||||
id: string;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Retrieves the attack discovery schedule. */
|
||||
export const getAttackDiscoverySchedule = async ({
|
||||
id,
|
||||
signal,
|
||||
}: GetAttackDiscoveryScheduleParams): Promise<GetAttackDiscoverySchedulesResponse> => {
|
||||
return KibanaServices.get().http.get<GetAttackDiscoverySchedulesResponse>(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id }),
|
||||
{ version: '1', signal }
|
||||
);
|
||||
};
|
||||
|
||||
export interface UpdateAttackDiscoveryScheduleParams {
|
||||
/** `id` of the attack discovery schedule */
|
||||
id: string;
|
||||
/** The body containing the schedule attributes */
|
||||
body: AttackDiscoveryScheduleUpdateProps;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Updates the attack discovery schedule. */
|
||||
export const updateAttackDiscoverySchedule = async ({
|
||||
id,
|
||||
body,
|
||||
signal,
|
||||
}: UpdateAttackDiscoveryScheduleParams): Promise<UpdateAttackDiscoverySchedulesResponse> => {
|
||||
return KibanaServices.get().http.put<UpdateAttackDiscoverySchedulesResponse>(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id }),
|
||||
{ body: JSON.stringify(body), version: '1', signal }
|
||||
);
|
||||
};
|
||||
|
||||
export interface DeleteAttackDiscoveryScheduleParams {
|
||||
/** `id` of the attack discovery schedule */
|
||||
id: string;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Deletes the attack discovery schedule. */
|
||||
export const deleteAttackDiscoverySchedule = async ({
|
||||
id,
|
||||
signal,
|
||||
}: DeleteAttackDiscoveryScheduleParams): Promise<DeleteAttackDiscoverySchedulesResponse> => {
|
||||
return KibanaServices.get().http.delete<DeleteAttackDiscoverySchedulesResponse>(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id }),
|
||||
{ version: '1', signal }
|
||||
);
|
||||
};
|
||||
|
||||
export interface EnableAttackDiscoveryScheduleParams {
|
||||
/** `id` of the attack discovery schedule */
|
||||
id: string;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Enables the attack discovery schedule. */
|
||||
export const enableAttackDiscoverySchedule = async ({
|
||||
id,
|
||||
signal,
|
||||
}: EnableAttackDiscoveryScheduleParams): Promise<EnableAttackDiscoverySchedulesResponse> => {
|
||||
return KibanaServices.get().http.post<EnableAttackDiscoverySchedulesResponse>(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE, { id }),
|
||||
{ version: '1', signal }
|
||||
);
|
||||
};
|
||||
|
||||
export interface DisableAttackDiscoveryScheduleParams {
|
||||
/** `id` of the attack discovery schedule */
|
||||
id: string;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Disables the attack discovery schedule. */
|
||||
export const disableAttackDiscoverySchedule = async ({
|
||||
id,
|
||||
signal,
|
||||
}: DisableAttackDiscoveryScheduleParams): Promise<DisableAttackDiscoverySchedulesResponse> => {
|
||||
return KibanaServices.get().http.post<DisableAttackDiscoverySchedulesResponse>(
|
||||
replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE, { id }),
|
||||
{ version: '1', signal }
|
||||
);
|
||||
};
|
||||
|
||||
export interface FindAttackDiscoveryScheduleParams {
|
||||
/** Optional page number to retrieve */
|
||||
page?: number;
|
||||
/** Optional number of documents per page to retrieve */
|
||||
perPage?: number;
|
||||
/** Optional field of the attack discovery schedule object to sort results by */
|
||||
sortField?: string;
|
||||
/** Optional direction to sort results by */
|
||||
sortDirection?: 'asc' | 'desc';
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Retrieves attack discovery schedules. */
|
||||
export const findAttackDiscoverySchedule = async ({
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortDirection,
|
||||
signal,
|
||||
}: FindAttackDiscoveryScheduleParams): Promise<FindAttackDiscoverySchedulesResponse> => {
|
||||
return KibanaServices.get().http.get<FindAttackDiscoverySchedulesResponse>(
|
||||
ATTACK_DISCOVERY_SCHEDULES_FIND,
|
||||
{ version: '1', query: { page, perPage, sortField, sortDirection }, signal }
|
||||
);
|
||||
};
|
|
@ -16,12 +16,21 @@ import {
|
|||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import { useAssistantContext, useLoadConnectors } from '@kbn/elastic-assistant';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { convertToBuildEsQuery } from '../../../../../common/lib/kuery';
|
||||
import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
||||
import { Footer } from '../../footer';
|
||||
import { MIN_FLYOUT_WIDTH } from '../../constants';
|
||||
import { useEditForm } from '../hooks/use_edit_form';
|
||||
import type { AttackDiscoveryScheduleSchema } from '../hooks/types';
|
||||
import { useEditForm } from '../edit_form';
|
||||
import type { AttackDiscoveryScheduleSchema } from '../edit_form/types';
|
||||
import { useCreateAttackDiscoverySchedule } from '../logic/use_create_schedule';
|
||||
import { parseFilterQuery } from '../../parse_filter_query';
|
||||
import { getGenAiConfig } from '../../../use_attack_discovery/helpers';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
@ -32,16 +41,78 @@ export const CreateFlyout: React.FC<Props> = React.memo(({ onClose }) => {
|
|||
prefix: 'attackDiscoveryScheduleCreateFlyoutTitle',
|
||||
});
|
||||
|
||||
const {
|
||||
services: { uiSettings },
|
||||
} = useKibana();
|
||||
|
||||
const { alertsIndexPattern, http } = useAssistantContext();
|
||||
const { data: aiConnectors, isLoading: isLoadingConnectors } = useLoadConnectors({
|
||||
http,
|
||||
});
|
||||
|
||||
const { indexPattern } = useSourcererDataView();
|
||||
|
||||
const { mutateAsync: createAttackDiscoverySchedule, isLoading: isLoadingQuery } =
|
||||
useCreateAttackDiscoverySchedule();
|
||||
|
||||
const onCreateSchedule = useCallback(
|
||||
(scheduleData: AttackDiscoveryScheduleSchema) => {
|
||||
// TODO: handle create schedule
|
||||
onClose();
|
||||
async (scheduleData: AttackDiscoveryScheduleSchema) => {
|
||||
const connector = aiConnectors?.find((item) => item.id === scheduleData.connectorId);
|
||||
if (!connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const genAiConfig = getGenAiConfig(connector);
|
||||
|
||||
const [filterQuery, kqlError] = convertToBuildEsQuery({
|
||||
config: getEsQueryConfig(uiSettings),
|
||||
indexPattern,
|
||||
queries: [scheduleData.alertsSelectionSettings.query],
|
||||
filters: scheduleData.alertsSelectionSettings.filters,
|
||||
});
|
||||
const filter = parseFilterQuery({ filterQuery, kqlError });
|
||||
|
||||
const apiConfig = {
|
||||
connectorId: connector.id,
|
||||
name: connector.name,
|
||||
actionTypeId: connector.actionTypeId,
|
||||
provider: connector.apiProvider,
|
||||
model: genAiConfig?.defaultModel,
|
||||
};
|
||||
const scheduleToCreate = {
|
||||
name: scheduleData.name,
|
||||
enabled: true,
|
||||
params: {
|
||||
alertsIndexPattern: alertsIndexPattern ?? '',
|
||||
apiConfig,
|
||||
end: scheduleData.alertsSelectionSettings.end,
|
||||
filter,
|
||||
size: scheduleData.alertsSelectionSettings.size,
|
||||
start: scheduleData.alertsSelectionSettings.start,
|
||||
},
|
||||
schedule: { interval: scheduleData.interval },
|
||||
actions: scheduleData.actions,
|
||||
};
|
||||
await createAttackDiscoverySchedule({ scheduleToCreate });
|
||||
onClose();
|
||||
} catch (err) {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
[
|
||||
aiConnectors,
|
||||
alertsIndexPattern,
|
||||
createAttackDiscoverySchedule,
|
||||
onClose,
|
||||
indexPattern,
|
||||
uiSettings,
|
||||
]
|
||||
);
|
||||
|
||||
const { editForm, actionButtons } = useEditForm({
|
||||
onSave: onCreateSchedule,
|
||||
saveButtonDisabled: isLoadingConnectors || isLoadingQuery,
|
||||
saveButtonTitle: i18n.SCHEDULE_CREATE_BUTTON_TITLE,
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './use_edit_form';
|
|
@ -49,11 +49,17 @@ export interface UseEditForm {
|
|||
export interface UseEditFormProps {
|
||||
initialValue?: AttackDiscoveryScheduleSchema;
|
||||
onSave?: (scheduleData: AttackDiscoveryScheduleSchema) => void;
|
||||
saveButtonDisabled?: boolean;
|
||||
saveButtonTitle?: string;
|
||||
}
|
||||
|
||||
export const useEditForm = (props: UseEditFormProps): UseEditForm => {
|
||||
const { initialValue = defaultInitialValue, onSave, saveButtonTitle } = props;
|
||||
const {
|
||||
initialValue = defaultInitialValue,
|
||||
onSave,
|
||||
saveButtonDisabled = false,
|
||||
saveButtonTitle,
|
||||
} = props;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const {
|
||||
triggersActionsUi: { actionTypeRegistry },
|
||||
|
@ -173,14 +179,20 @@ export const useEditForm = (props: UseEditFormProps): UseEditForm => {
|
|||
grow={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton data-test-subj="save" fill onClick={onCreate} size="s">
|
||||
<EuiButton
|
||||
data-test-subj="save"
|
||||
fill
|
||||
size="s"
|
||||
onClick={onCreate}
|
||||
disabled={saveButtonDisabled}
|
||||
>
|
||||
{saveButtonTitle ?? i18n.SCHEDULE_SAVE_BUTTON_TITLE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [euiTheme.size.s, onCreate, saveButtonTitle]);
|
||||
}, [euiTheme.size.s, onCreate, saveButtonDisabled, saveButtonTitle]);
|
||||
|
||||
return { editForm, actionButtons };
|
||||
};
|
|
@ -17,11 +17,13 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import ScheduleIconSVG from './icons/schedule.svg';
|
||||
import * as i18n from './translations';
|
||||
import { CreateFlyout } from './create_flyout';
|
||||
|
||||
export const EmptySchedule: React.FC = React.memo(() => {
|
||||
import * as i18n from './translations';
|
||||
|
||||
import ScheduleIconSVG from '../icons/schedule.svg';
|
||||
import { CreateFlyout } from '../create_flyout';
|
||||
|
||||
export const EmptyPage: React.FC = React.memo(() => {
|
||||
// showing / hiding the flyout:
|
||||
const [showFlyout, setShowFlyout] = useState<boolean>(false);
|
||||
const openFlyout = useCallback(() => setShowFlyout(true), []);
|
||||
|
@ -80,4 +82,4 @@ export const EmptySchedule: React.FC = React.memo(() => {
|
|||
</>
|
||||
);
|
||||
});
|
||||
EmptySchedule.displayName = 'EmptySchedule';
|
||||
EmptyPage.displayName = 'EmptyPage';
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
const ONE_MINUTE = 60000;
|
||||
|
||||
export const DEFAULT_QUERY_OPTIONS = {
|
||||
refetchIntervalInBackground: false,
|
||||
staleTime: ONE_MINUTE * 5,
|
||||
retry: false,
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FETCH_ATTACK_DISCOVERY_SCHEDULES_FAILURE = (single = true) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.fetchSchedulesFailDescription', {
|
||||
defaultMessage:
|
||||
'Failed to fetch {failed, plural, one {attack discovery schedule} other {attack discovery schedules}}',
|
||||
values: { failed: single ? 1 : 2 },
|
||||
});
|
||||
|
||||
export const CREATE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS = (succeeded = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.createSchedulesSuccess', {
|
||||
defaultMessage:
|
||||
'{succeeded, plural, one {# attack discovery schedule} other {# attack discovery schedules}} created successfully.',
|
||||
values: { succeeded },
|
||||
});
|
||||
|
||||
export const CREATE_ATTACK_DISCOVERY_SCHEDULES_FAILURE = (failed = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.createSchedulesFailDescription', {
|
||||
defaultMessage:
|
||||
'Failed to create {failed, plural, one {# attack discovery schedule} other {# attack discovery schedules}}',
|
||||
values: { failed },
|
||||
});
|
||||
|
||||
export const UPDATE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS = (succeeded = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.updateSchedulesSuccess', {
|
||||
defaultMessage:
|
||||
'{succeeded, plural, one {# attack discovery schedule} other {# attack discovery schedules}} updated successfully.',
|
||||
values: { succeeded },
|
||||
});
|
||||
|
||||
export const UPDATE_ATTACK_DISCOVERY_SCHEDULES_FAILURE = (failed = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.updateSchedulesFailDescription', {
|
||||
defaultMessage:
|
||||
'Failed to update {failed, plural, one {# attack discovery schedule} other {# attack discovery schedules}}',
|
||||
values: { failed },
|
||||
});
|
||||
|
||||
export const DELETE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS = (succeeded = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.deleteSchedulesSuccess', {
|
||||
defaultMessage:
|
||||
'{succeeded, plural, one {# attack discovery schedule} other {# attack discovery schedules}} deleted successfully.',
|
||||
values: { succeeded },
|
||||
});
|
||||
|
||||
export const DELETE_ATTACK_DISCOVERY_SCHEDULES_FAILURE = (failed = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.deleteSchedulesFailDescription', {
|
||||
defaultMessage:
|
||||
'Failed to delete {failed, plural, one {# attack discovery schedule} other {# attack discovery schedules}}',
|
||||
values: { failed },
|
||||
});
|
||||
|
||||
export const ENABLE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS = (succeeded = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.enableSchedulesSuccess', {
|
||||
defaultMessage:
|
||||
'{succeeded, plural, one {# attack discovery schedule} other {# attack discovery schedules}} enabled successfully.',
|
||||
values: { succeeded },
|
||||
});
|
||||
|
||||
export const ENABLE_ATTACK_DISCOVERY_SCHEDULES_FAILURE = (failed = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.enableSchedulesFailDescription', {
|
||||
defaultMessage:
|
||||
'Failed to enable {failed, plural, one {# attack discovery schedule} other {# attack discovery schedules}}',
|
||||
values: { failed },
|
||||
});
|
||||
|
||||
export const DISABLE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS = (succeeded = 1) =>
|
||||
i18n.translate('xpack.securitySolution.attackDiscovery.schedule.disableSchedulesSuccess', {
|
||||
defaultMessage:
|
||||
'{succeeded, plural, one {# attack discovery schedule} other {# attack discovery schedules}} disabled successfully.',
|
||||
values: { succeeded },
|
||||
});
|
||||
|
||||
export const DISABLE_ATTACK_DISCOVERY_SCHEDULES_FAILURE = (failed = 1) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.schedule.disableSchedulesFailDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Failed to disable {failed, plural, one {# attack discovery schedule} other {# attack discovery schedules}}',
|
||||
values: { failed },
|
||||
}
|
||||
);
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { useCreateAttackDiscoverySchedule } from './use_create_schedule';
|
||||
import { mockCreateAttackDiscoverySchedule } from '../../../mock/mock_attack_discovery_schedule';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { renderMutation } from '../../../../../management/hooks/test_utils';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { createAttackDiscoverySchedule } from '../api';
|
||||
|
||||
jest.mock('./use_find_schedules');
|
||||
jest.mock('../api');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const createAttackDiscoveryScheduleMock = createAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof createAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateFindAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateFindAttackDiscoverySchedule =
|
||||
useInvalidateFindAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateFindAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
describe('useCreateAttackDiscoverySchedule', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
|
||||
createAttackDiscoveryScheduleMock.mockReturnValue(
|
||||
{} as unknown as jest.Mocked<ReturnType<typeof createAttackDiscoverySchedule>>
|
||||
);
|
||||
|
||||
mockUseInvalidateFindAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateFindAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateFindAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `createAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useCreateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ scheduleToCreate: mockCreateAttackDiscoverySchedule });
|
||||
expect(createAttackDiscoveryScheduleMock).toHaveBeenCalledWith({
|
||||
body: mockCreateAttackDiscoverySchedule,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addSuccess`', async () => {
|
||||
const result = await renderMutation(() => useCreateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ scheduleToCreate: mockCreateAttackDiscoverySchedule });
|
||||
expect(appToastsMock.addSuccess).toHaveBeenCalledWith(
|
||||
'1 attack discovery schedule created successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateFindAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useCreateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ scheduleToCreate: mockCreateAttackDiscoverySchedule });
|
||||
expect(invalidateFindAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addError`', async () => {
|
||||
createAttackDiscoveryScheduleMock.mockRejectedValue('Royally failed!');
|
||||
|
||||
const result = await renderMutation(() => useCreateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.mutateAsync({ scheduleToCreate: mockCreateAttackDiscoverySchedule });
|
||||
} catch (err) {
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith('Royally failed!', {
|
||||
title: 'Failed to create 1 attack discovery schedule',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type {
|
||||
AttackDiscoveryScheduleCreateProps,
|
||||
CreateAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { createAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
|
||||
export const CREATE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY = ['POST', ATTACK_DISCOVERY_SCHEDULES];
|
||||
|
||||
interface CreateAttackDiscoveryScheduleParams {
|
||||
scheduleToCreate: AttackDiscoveryScheduleCreateProps;
|
||||
}
|
||||
|
||||
export const useCreateAttackDiscoverySchedule = () => {
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
|
||||
const invalidateFindAttackDiscoverySchedule = useInvalidateFindAttackDiscoverySchedule();
|
||||
|
||||
return useMutation<
|
||||
CreateAttackDiscoverySchedulesResponse,
|
||||
Error,
|
||||
CreateAttackDiscoveryScheduleParams
|
||||
>(({ scheduleToCreate }) => createAttackDiscoverySchedule({ body: scheduleToCreate }), {
|
||||
mutationKey: CREATE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY,
|
||||
onSuccess: () => {
|
||||
invalidateFindAttackDiscoverySchedule();
|
||||
addSuccess(i18n.CREATE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS());
|
||||
},
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.CREATE_ATTACK_DISCOVERY_SCHEDULES_FAILURE() });
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { useDeleteAttackDiscoverySchedule } from './use_delete_schedule';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { renderMutation } from '../../../../../management/hooks/test_utils';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { deleteAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
|
||||
jest.mock('./use_find_schedules');
|
||||
jest.mock('./use_get_schedule');
|
||||
jest.mock('../api');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const deleteAttackDiscoveryScheduleMock = deleteAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof deleteAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateFindAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateFindAttackDiscoverySchedule =
|
||||
useInvalidateFindAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateFindAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateGetAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateGetAttackDiscoverySchedule =
|
||||
useInvalidateGetAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateGetAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
describe('useDeleteAttackDiscoverySchedule', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
|
||||
deleteAttackDiscoveryScheduleMock.mockReturnValue(
|
||||
{} as unknown as jest.Mocked<ReturnType<typeof deleteAttackDiscoverySchedule>>
|
||||
);
|
||||
|
||||
mockUseInvalidateFindAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateFindAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateFindAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
mockUseInvalidateGetAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateGetAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateGetAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `deleteAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useDeleteAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-0' });
|
||||
expect(deleteAttackDiscoveryScheduleMock).toHaveBeenCalledWith({ id: 'test-0' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addSuccess`', async () => {
|
||||
const result = await renderMutation(() => useDeleteAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-1' });
|
||||
expect(appToastsMock.addSuccess).toHaveBeenCalledWith(
|
||||
'1 attack discovery schedule deleted successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateFindAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useDeleteAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-2' });
|
||||
expect(invalidateFindAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateGetAttackDiscoveryScheduleMock`', async () => {
|
||||
const result = await renderMutation(() => useDeleteAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-3' });
|
||||
expect(invalidateGetAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addError`', async () => {
|
||||
deleteAttackDiscoveryScheduleMock.mockRejectedValue('Royally failed!');
|
||||
|
||||
const result = await renderMutation(() => useDeleteAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.mutateAsync({ id: 'test-4' });
|
||||
} catch (err) {
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith('Royally failed!', {
|
||||
title: 'Failed to delete 1 attack discovery schedule',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { DeleteAttackDiscoverySchedulesResponse } from '@kbn/elastic-assistant-common';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { deleteAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
|
||||
export const DELETE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY = [
|
||||
'DELETE',
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
];
|
||||
|
||||
interface DeleteAttackDiscoveryScheduleParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const useDeleteAttackDiscoverySchedule = () => {
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
|
||||
const invalidateGetAttackDiscoverySchedule = useInvalidateGetAttackDiscoverySchedule();
|
||||
const invalidateFindAttackDiscoverySchedule = useInvalidateFindAttackDiscoverySchedule();
|
||||
|
||||
return useMutation<
|
||||
DeleteAttackDiscoverySchedulesResponse,
|
||||
Error,
|
||||
DeleteAttackDiscoveryScheduleParams
|
||||
>(({ id }) => deleteAttackDiscoverySchedule({ id }), {
|
||||
mutationKey: DELETE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY,
|
||||
onSuccess: ({ id }) => {
|
||||
invalidateGetAttackDiscoverySchedule(id);
|
||||
invalidateFindAttackDiscoverySchedule();
|
||||
addSuccess(i18n.DELETE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS());
|
||||
},
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.DELETE_ATTACK_DISCOVERY_SCHEDULES_FAILURE() });
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { useDisableAttackDiscoverySchedule } from './use_disable_schedule';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { renderMutation } from '../../../../../management/hooks/test_utils';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { disableAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
|
||||
jest.mock('./use_find_schedules');
|
||||
jest.mock('./use_get_schedule');
|
||||
jest.mock('../api');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const disableAttackDiscoveryScheduleMock = disableAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof disableAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateFindAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateFindAttackDiscoverySchedule =
|
||||
useInvalidateFindAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateFindAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateGetAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateGetAttackDiscoverySchedule =
|
||||
useInvalidateGetAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateGetAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
describe('useDisableAttackDiscoverySchedule', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
|
||||
disableAttackDiscoveryScheduleMock.mockReturnValue(
|
||||
{} as unknown as jest.Mocked<ReturnType<typeof disableAttackDiscoverySchedule>>
|
||||
);
|
||||
|
||||
mockUseInvalidateFindAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateFindAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateFindAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
mockUseInvalidateGetAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateGetAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateGetAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `disableAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useDisableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-0' });
|
||||
expect(disableAttackDiscoveryScheduleMock).toHaveBeenCalledWith({ id: 'test-0' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addSuccess`', async () => {
|
||||
const result = await renderMutation(() => useDisableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-1' });
|
||||
expect(appToastsMock.addSuccess).toHaveBeenCalledWith(
|
||||
'1 attack discovery schedule disabled successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateFindAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useDisableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-2' });
|
||||
expect(invalidateFindAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateGetAttackDiscoveryScheduleMock`', async () => {
|
||||
const result = await renderMutation(() => useDisableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-3' });
|
||||
expect(invalidateGetAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addError`', async () => {
|
||||
disableAttackDiscoveryScheduleMock.mockRejectedValue('Royally failed!');
|
||||
|
||||
const result = await renderMutation(() => useDisableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.mutateAsync({ id: 'test-4' });
|
||||
} catch (err) {
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith('Royally failed!', {
|
||||
title: 'Failed to disable 1 attack discovery schedule',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { DisableAttackDiscoverySchedulesResponse } from '@kbn/elastic-assistant-common';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { disableAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
|
||||
export const DISABLE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY = [
|
||||
'POST',
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE,
|
||||
];
|
||||
|
||||
interface DisableAttackDiscoveryScheduleParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const useDisableAttackDiscoverySchedule = () => {
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
|
||||
const invalidateGetAttackDiscoverySchedule = useInvalidateGetAttackDiscoverySchedule();
|
||||
const invalidateFindAttackDiscoverySchedule = useInvalidateFindAttackDiscoverySchedule();
|
||||
|
||||
return useMutation<
|
||||
DisableAttackDiscoverySchedulesResponse,
|
||||
Error,
|
||||
DisableAttackDiscoveryScheduleParams
|
||||
>(({ id }) => disableAttackDiscoverySchedule({ id }), {
|
||||
mutationKey: DISABLE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY,
|
||||
onSuccess: ({ id }) => {
|
||||
invalidateGetAttackDiscoverySchedule(id);
|
||||
invalidateFindAttackDiscoverySchedule();
|
||||
addSuccess(i18n.DISABLE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS());
|
||||
},
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.DISABLE_ATTACK_DISCOVERY_SCHEDULES_FAILURE() });
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { useEnableAttackDiscoverySchedule } from './use_enable_schedule';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { renderMutation } from '../../../../../management/hooks/test_utils';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { enableAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
|
||||
jest.mock('./use_find_schedules');
|
||||
jest.mock('./use_get_schedule');
|
||||
jest.mock('../api');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const enableAttackDiscoveryScheduleMock = enableAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof enableAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateFindAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateFindAttackDiscoverySchedule =
|
||||
useInvalidateFindAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateFindAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateGetAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateGetAttackDiscoverySchedule =
|
||||
useInvalidateGetAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateGetAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
describe('useEnableAttackDiscoverySchedule', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
|
||||
enableAttackDiscoveryScheduleMock.mockReturnValue(
|
||||
{} as unknown as jest.Mocked<ReturnType<typeof enableAttackDiscoverySchedule>>
|
||||
);
|
||||
|
||||
mockUseInvalidateFindAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateFindAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateFindAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
mockUseInvalidateGetAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateGetAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateGetAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `enableAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useEnableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-0' });
|
||||
expect(enableAttackDiscoveryScheduleMock).toHaveBeenCalledWith({ id: 'test-0' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addSuccess`', async () => {
|
||||
const result = await renderMutation(() => useEnableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-1' });
|
||||
expect(appToastsMock.addSuccess).toHaveBeenCalledWith(
|
||||
'1 attack discovery schedule enabled successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateFindAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useEnableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-2' });
|
||||
expect(invalidateFindAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateGetAttackDiscoveryScheduleMock`', async () => {
|
||||
const result = await renderMutation(() => useEnableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-3' });
|
||||
expect(invalidateGetAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addError`', async () => {
|
||||
enableAttackDiscoveryScheduleMock.mockRejectedValue('Royally failed!');
|
||||
|
||||
const result = await renderMutation(() => useEnableAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.mutateAsync({ id: 'test-4' });
|
||||
} catch (err) {
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith('Royally failed!', {
|
||||
title: 'Failed to enable 1 attack discovery schedule',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { EnableAttackDiscoverySchedulesResponse } from '@kbn/elastic-assistant-common';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { enableAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
|
||||
export const ENABLE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY = [
|
||||
'POST',
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID_ENABLE,
|
||||
];
|
||||
|
||||
interface EnableAttackDiscoveryScheduleParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const useEnableAttackDiscoverySchedule = () => {
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
|
||||
const invalidateGetAttackDiscoverySchedule = useInvalidateGetAttackDiscoverySchedule();
|
||||
const invalidateFindAttackDiscoverySchedule = useInvalidateFindAttackDiscoverySchedule();
|
||||
|
||||
return useMutation<
|
||||
EnableAttackDiscoverySchedulesResponse,
|
||||
Error,
|
||||
EnableAttackDiscoveryScheduleParams
|
||||
>(({ id }) => enableAttackDiscoverySchedule({ id }), {
|
||||
mutationKey: ENABLE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY,
|
||||
onSuccess: ({ id }) => {
|
||||
invalidateGetAttackDiscoverySchedule(id);
|
||||
invalidateFindAttackDiscoverySchedule();
|
||||
addSuccess(i18n.ENABLE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS());
|
||||
},
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.ENABLE_ATTACK_DISCOVERY_SCHEDULES_FAILURE() });
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useFindAttackDiscoverySchedules } from './use_find_schedules';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { renderQuery } from '../../../../../management/hooks/test_utils';
|
||||
import { findAttackDiscoverySchedule } from '../api';
|
||||
|
||||
jest.mock('../api');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const findAttackDiscoveryScheduleMock = findAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof findAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
describe('useFindAttackDiscoverySchedules', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
|
||||
findAttackDiscoveryScheduleMock.mockReturnValue(
|
||||
{} as unknown as jest.Mocked<ReturnType<typeof findAttackDiscoverySchedule>>
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `findAttackDiscoverySchedule`', async () => {
|
||||
await renderQuery(() => useFindAttackDiscoverySchedules({ page: 1 }), 'isSuccess');
|
||||
|
||||
expect(findAttackDiscoveryScheduleMock).toHaveBeenCalledWith({
|
||||
page: 1,
|
||||
signal: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addError`', async () => {
|
||||
findAttackDiscoveryScheduleMock.mockRejectedValue('Royally failed!');
|
||||
|
||||
await renderQuery(() => useFindAttackDiscoverySchedules({ page: 2 }), 'isError');
|
||||
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith('Royally failed!', {
|
||||
title: 'Failed to fetch attack discovery schedules',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_FIND } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { findAttackDiscoverySchedule } from '../api';
|
||||
import { DEFAULT_QUERY_OPTIONS } from './constants';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
|
||||
export const useFindAttackDiscoverySchedules = (params?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortField?: string;
|
||||
sortDirection?: 'asc' | 'desc';
|
||||
}) => {
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
return useQuery(
|
||||
['GET', ATTACK_DISCOVERY_SCHEDULES_FIND, params],
|
||||
async ({ signal }) => {
|
||||
const response = await findAttackDiscoverySchedule({ signal, ...params });
|
||||
|
||||
return { schedules: response.data, total: response.total };
|
||||
},
|
||||
{
|
||||
...DEFAULT_QUERY_OPTIONS,
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.FETCH_ATTACK_DISCOVERY_SCHEDULES_FAILURE(false) });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* We should use this hook to invalidate the attack discovery schedule cache. For
|
||||
* example, attack discovery schedule mutations, like create a schedule, should lead to cache invalidation.
|
||||
*
|
||||
* @returns A attack discovery schedule cache invalidation callback
|
||||
*/
|
||||
export const useInvalidateFindAttackDiscoverySchedule = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useCallback(() => {
|
||||
/**
|
||||
* Invalidate all queries that start with ATTACK_DISCOVERY_SCHEDULES_FIND. This
|
||||
* includes the in-memory query cache and paged query cache.
|
||||
*/
|
||||
queryClient.invalidateQueries(['GET', ATTACK_DISCOVERY_SCHEDULES_FIND], {
|
||||
refetchType: 'active',
|
||||
});
|
||||
}, [queryClient]);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { renderQuery } from '../../../../../management/hooks/test_utils';
|
||||
import { getAttackDiscoverySchedule } from '../api';
|
||||
|
||||
jest.mock('../api');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const getAttackDiscoveryScheduleMock = getAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof getAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
describe('useGetAttackDiscoverySchedule', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
|
||||
getAttackDiscoveryScheduleMock.mockReturnValue(
|
||||
{} as unknown as jest.Mocked<ReturnType<typeof getAttackDiscoverySchedule>>
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `getAttackDiscoverySchedule`', async () => {
|
||||
await renderQuery(() => useGetAttackDiscoverySchedule({ id: 'test-1' }), 'isSuccess');
|
||||
|
||||
expect(getAttackDiscoveryScheduleMock).toHaveBeenCalledWith({
|
||||
id: 'test-1',
|
||||
signal: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addError`', async () => {
|
||||
getAttackDiscoveryScheduleMock.mockRejectedValue('Royally failed!');
|
||||
|
||||
await renderQuery(() => useGetAttackDiscoverySchedule({ id: 'test-2' }), 'isError');
|
||||
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith('Royally failed!', {
|
||||
title: 'Failed to fetch attack discovery schedule',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { replaceParams } from '@kbn/openapi-common/shared';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { getAttackDiscoverySchedule } from '../api';
|
||||
import { DEFAULT_QUERY_OPTIONS } from './constants';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
|
||||
export const useGetAttackDiscoverySchedule = (params: { id: string }) => {
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
const { id } = params;
|
||||
const SPECIFIC_PATH = replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id });
|
||||
|
||||
return useQuery(
|
||||
['GET', SPECIFIC_PATH, params],
|
||||
async ({ signal }) => {
|
||||
const response = await getAttackDiscoverySchedule({ signal, ...params });
|
||||
|
||||
return { schedule: response };
|
||||
},
|
||||
{
|
||||
...DEFAULT_QUERY_OPTIONS,
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.FETCH_ATTACK_DISCOVERY_SCHEDULES_FAILURE() });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* We should use this hook to invalidate the attack discovery schedule cache. For
|
||||
* example, attack discovery schedule mutations, like create a schedule, should lead to cache invalidation.
|
||||
*
|
||||
* @returns A attack discovery schedule cache invalidation callback
|
||||
*/
|
||||
export const useInvalidateGetAttackDiscoverySchedule = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useCallback(
|
||||
(id: string) => {
|
||||
const SPECIFIC_PATH = replaceParams(ATTACK_DISCOVERY_SCHEDULES_BY_ID, { id });
|
||||
|
||||
/**
|
||||
* Invalidate all queries that start with SPECIFIC_PATH. This
|
||||
* includes the in-memory query cache and paged query cache.
|
||||
*/
|
||||
queryClient.invalidateQueries(['GET', SPECIFIC_PATH], {
|
||||
refetchType: 'active',
|
||||
});
|
||||
},
|
||||
[queryClient]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { useUpdateAttackDiscoverySchedule } from './use_update_schedule';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
|
||||
import { renderMutation } from '../../../../../management/hooks/test_utils';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { updateAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
|
||||
jest.mock('./use_find_schedules');
|
||||
jest.mock('./use_get_schedule');
|
||||
jest.mock('../api');
|
||||
jest.mock('../../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const updateAttackDiscoveryScheduleMock = updateAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof updateAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateFindAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateFindAttackDiscoverySchedule =
|
||||
useInvalidateFindAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateFindAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
const invalidateGetAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseInvalidateGetAttackDiscoverySchedule =
|
||||
useInvalidateGetAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useInvalidateGetAttackDiscoverySchedule
|
||||
>;
|
||||
|
||||
describe('useUpdateAttackDiscoverySchedule', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
|
||||
updateAttackDiscoveryScheduleMock.mockReturnValue(
|
||||
{} as unknown as jest.Mocked<ReturnType<typeof updateAttackDiscoverySchedule>>
|
||||
);
|
||||
|
||||
mockUseInvalidateFindAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateFindAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateFindAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
mockUseInvalidateGetAttackDiscoverySchedule.mockReturnValue(
|
||||
invalidateGetAttackDiscoveryScheduleMock as unknown as jest.Mocked<
|
||||
ReturnType<typeof useInvalidateGetAttackDiscoverySchedule>
|
||||
>
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `updateAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useUpdateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-0' });
|
||||
expect(updateAttackDiscoveryScheduleMock).toHaveBeenCalledWith({ id: 'test-0' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addSuccess`', async () => {
|
||||
const result = await renderMutation(() => useUpdateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-1' });
|
||||
expect(appToastsMock.addSuccess).toHaveBeenCalledWith(
|
||||
'1 attack discovery schedule updated successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateFindAttackDiscoverySchedule`', async () => {
|
||||
const result = await renderMutation(() => useUpdateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-2' });
|
||||
expect(invalidateFindAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `invalidateGetAttackDiscoveryScheduleMock`', async () => {
|
||||
const result = await renderMutation(() => useUpdateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
await result.mutateAsync({ id: 'test-3' });
|
||||
expect(invalidateGetAttackDiscoveryScheduleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `addError`', async () => {
|
||||
updateAttackDiscoveryScheduleMock.mockRejectedValue('Royally failed!');
|
||||
|
||||
const result = await renderMutation(() => useUpdateAttackDiscoverySchedule());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.mutateAsync({ id: 'test-4' });
|
||||
} catch (err) {
|
||||
expect(appToastsMock.addError).toHaveBeenCalledWith('Royally failed!', {
|
||||
title: 'Failed to update 1 attack discovery schedule',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type {
|
||||
AttackDiscoveryScheduleUpdateProps,
|
||||
UpdateAttackDiscoverySchedulesResponse,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_BY_ID } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { updateAttackDiscoverySchedule } from '../api';
|
||||
import { useInvalidateGetAttackDiscoverySchedule } from './use_get_schedule';
|
||||
import { useInvalidateFindAttackDiscoverySchedule } from './use_find_schedules';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
|
||||
export const UPDATE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY = [
|
||||
'PUT',
|
||||
ATTACK_DISCOVERY_SCHEDULES_BY_ID,
|
||||
];
|
||||
|
||||
interface UpdateAttackDiscoveryScheduleParams {
|
||||
id: string;
|
||||
scheduleToUpdate: AttackDiscoveryScheduleUpdateProps;
|
||||
}
|
||||
|
||||
export const useUpdateAttackDiscoverySchedule = () => {
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
|
||||
const invalidateGetAttackDiscoverySchedule = useInvalidateGetAttackDiscoverySchedule();
|
||||
const invalidateFindAttackDiscoverySchedule = useInvalidateFindAttackDiscoverySchedule();
|
||||
|
||||
return useMutation<
|
||||
UpdateAttackDiscoverySchedulesResponse,
|
||||
Error,
|
||||
UpdateAttackDiscoveryScheduleParams
|
||||
>(({ id, scheduleToUpdate }) => updateAttackDiscoverySchedule({ id, body: scheduleToUpdate }), {
|
||||
mutationKey: UPDATE_ATTACK_DISCOVERY_SCHEDULE_MUTATION_KEY,
|
||||
onSuccess: ({ id }) => {
|
||||
invalidateGetAttackDiscoverySchedule(id);
|
||||
invalidateFindAttackDiscoverySchedule();
|
||||
addSuccess(i18n.UPDATE_ATTACK_DISCOVERY_SCHEDULES_SUCCESS());
|
||||
},
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.UPDATE_ATTACK_DISCOVERY_SCHEDULES_FAILURE() });
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { EuiTableFieldDataColumnType } from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { createActionsColumn } from './actions';
|
||||
import { TestProviders } from '../../../../../../common/mock';
|
||||
import { mockAttackDiscoverySchedule } from '../../../../mock/mock_attack_discovery_schedule';
|
||||
|
||||
const deleteScheduleMock = jest.fn();
|
||||
|
||||
describe('Actions Column', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const column = createActionsColumn({
|
||||
isDisabled: false,
|
||||
deleteSchedule: deleteScheduleMock,
|
||||
}) as EuiTableFieldDataColumnType<AttackDiscoverySchedule>;
|
||||
|
||||
render(<TestProviders>{column.render?.('', mockAttackDiscoverySchedule)}</TestProviders>);
|
||||
});
|
||||
|
||||
it('should render delete button', () => {
|
||||
expect(screen.getByTestId('deleteButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should invoke `deleteSchedule` when the delete button is clicked', async () => {
|
||||
const deleteButton = screen.getByTestId('deleteButton');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(deleteScheduleMock).toHaveBeenCalledWith(mockAttackDiscoverySchedule.id);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { TableColumn } from './constants';
|
||||
|
||||
interface ActionProps {
|
||||
deleteSchedule: (scheduleId: string) => Promise<void>;
|
||||
isDisabled: boolean;
|
||||
scheduleId: string;
|
||||
}
|
||||
|
||||
const Action = ({ isDisabled, deleteSchedule, scheduleId }: ActionProps) => {
|
||||
const onScheduleDeleteChange = useCallback(async () => {
|
||||
deleteSchedule(scheduleId);
|
||||
}, [deleteSchedule, scheduleId]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="deleteButton"
|
||||
aria-label={i18n.DELETE_ACTIONS_BUTTON_ARIAL_LABEL}
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={onScheduleDeleteChange}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const createActionsColumn = ({
|
||||
isDisabled,
|
||||
deleteSchedule,
|
||||
}: {
|
||||
isDisabled: boolean;
|
||||
deleteSchedule: (scheduleId: string) => Promise<void>;
|
||||
}): TableColumn => {
|
||||
return {
|
||||
field: 'delete',
|
||||
name: i18n.COLUMN_ACTIONS,
|
||||
render: (_, schedule: AttackDiscoverySchedule) => (
|
||||
<Action isDisabled={isDisabled} deleteSchedule={deleteSchedule} scheduleId={schedule.id} />
|
||||
),
|
||||
width: '65px',
|
||||
align: 'center',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
export type TableColumn = EuiBasicTableColumn<AttackDiscoverySchedule>;
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { EuiTableFieldDataColumnType } from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { createEnableColumn } from './enable';
|
||||
import { TestProviders } from '../../../../../../common/mock';
|
||||
import { mockAttackDiscoverySchedule } from '../../../../mock/mock_attack_discovery_schedule';
|
||||
|
||||
const onSwitchChangeMock = jest.fn();
|
||||
|
||||
const renderEnabledSchedule = (enabled = true) => {
|
||||
const column = createEnableColumn({
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
onSwitchChange: onSwitchChangeMock,
|
||||
}) as EuiTableFieldDataColumnType<AttackDiscoverySchedule>;
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
{column.render?.('', { ...mockAttackDiscoverySchedule, enabled })}
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Enable Column', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render enable button', () => {
|
||||
renderEnabledSchedule();
|
||||
expect(screen.getByTestId('scheduleSwitch')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render enable button as checked if schedule is enabled', () => {
|
||||
renderEnabledSchedule(true);
|
||||
expect(screen.getByTestId('scheduleSwitch')).toBeChecked();
|
||||
});
|
||||
|
||||
it('should render enable button as not-checked if schedule is enabled', () => {
|
||||
renderEnabledSchedule(false);
|
||||
expect(screen.getByTestId('scheduleSwitch')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should invoke `onSwitchChange` with correct parameters for the enabled schedule', async () => {
|
||||
renderEnabledSchedule(true);
|
||||
|
||||
const deleteButton = screen.getByTestId('scheduleSwitch');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(onSwitchChangeMock).toHaveBeenCalledWith(mockAttackDiscoverySchedule.id, false);
|
||||
});
|
||||
|
||||
it('should invoke `onSwitchChange` with correct parameters for the disabled schedule', async () => {
|
||||
renderEnabledSchedule(false);
|
||||
|
||||
const deleteButton = screen.getByTestId('scheduleSwitch');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(onSwitchChangeMock).toHaveBeenCalledWith(mockAttackDiscoverySchedule.id, true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiSwitch,
|
||||
type EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { TableColumn } from './constants';
|
||||
|
||||
interface EnableSwitchProps {
|
||||
enabled: boolean;
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
onSwitchChange: (scheduleId: string, enabled: boolean) => Promise<void>;
|
||||
scheduleId: string;
|
||||
}
|
||||
|
||||
const EnableSwitch = ({
|
||||
enabled,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
onSwitchChange,
|
||||
scheduleId,
|
||||
}: EnableSwitchProps) => {
|
||||
const [myIsLoading, setMyIsLoading] = useState(false);
|
||||
|
||||
const onScheduleStateChange = useCallback(
|
||||
async (event: EuiSwitchEvent) => {
|
||||
setMyIsLoading(true);
|
||||
await onSwitchChange(scheduleId, !enabled);
|
||||
setMyIsLoading(false);
|
||||
},
|
||||
[enabled, onSwitchChange, scheduleId]
|
||||
);
|
||||
|
||||
const showLoader = useMemo((): boolean => {
|
||||
if (myIsLoading !== isLoading) {
|
||||
return isLoading || myIsLoading;
|
||||
}
|
||||
return myIsLoading;
|
||||
}, [myIsLoading, isLoading]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
{showLoader ? (
|
||||
<EuiLoadingSpinner size="m" data-test-subj="scheduleSwitchLoader" />
|
||||
) : (
|
||||
<EuiSwitch
|
||||
data-test-subj="scheduleSwitch"
|
||||
showLabel={false}
|
||||
label=""
|
||||
disabled={isDisabled}
|
||||
checked={enabled}
|
||||
onChange={onScheduleStateChange}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const createEnableColumn = ({
|
||||
isDisabled,
|
||||
isLoading,
|
||||
onSwitchChange,
|
||||
}: {
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
onSwitchChange: (scheduleId: string, enabled: boolean) => Promise<void>;
|
||||
}): TableColumn => {
|
||||
return {
|
||||
field: 'enabled',
|
||||
name: i18n.COLUMN_ENABLE,
|
||||
render: (_, schedule: AttackDiscoverySchedule) => (
|
||||
<EnableSwitch
|
||||
enabled={schedule.enabled}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
onSwitchChange={onSwitchChange}
|
||||
scheduleId={schedule.id}
|
||||
/>
|
||||
),
|
||||
width: '65px',
|
||||
align: 'center',
|
||||
};
|
||||
};
|
|
@ -5,10 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EmptySchedule } from './empty_schedule';
|
||||
export * from './constants';
|
||||
|
||||
export const Schedule: React.FC = React.memo(() => {
|
||||
return <EmptySchedule />;
|
||||
});
|
||||
Schedule.displayName = 'Schedule';
|
||||
export * from './actions';
|
||||
export * from './enable';
|
||||
export * from './name';
|
||||
export * from './status';
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { EuiTableFieldDataColumnType } from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { createNameColumn } from './name';
|
||||
import { TestProviders } from '../../../../../../common/mock';
|
||||
import { mockAttackDiscoverySchedule } from '../../../../mock/mock_attack_discovery_schedule';
|
||||
|
||||
const openScheduleDetailsMock = jest.fn();
|
||||
|
||||
describe('Name Column', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const column = createNameColumn({
|
||||
openScheduleDetails: openScheduleDetailsMock,
|
||||
}) as EuiTableFieldDataColumnType<AttackDiscoverySchedule>;
|
||||
|
||||
render(<TestProviders>{column.render?.('', mockAttackDiscoverySchedule)}</TestProviders>);
|
||||
});
|
||||
|
||||
it('should render schedule details link', () => {
|
||||
expect(screen.getByTestId('scheduleName')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should invoke `openScheduleDetails` when the name link is clicked', async () => {
|
||||
const detailsLink = screen.getByTestId('scheduleName');
|
||||
fireEvent.click(detailsLink);
|
||||
|
||||
expect(openScheduleDetailsMock).toHaveBeenCalledWith(mockAttackDiscoverySchedule.id);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { TableColumn } from './constants';
|
||||
|
||||
interface NameProps {
|
||||
schedule: AttackDiscoverySchedule;
|
||||
openScheduleDetails: (scheduleId: string) => void;
|
||||
}
|
||||
|
||||
const Name = ({ schedule, openScheduleDetails }: NameProps) => {
|
||||
return (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
openScheduleDetails(schedule.id);
|
||||
}}
|
||||
data-test-subj="scheduleName"
|
||||
>
|
||||
{schedule.name}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const createNameColumn = ({
|
||||
openScheduleDetails,
|
||||
}: {
|
||||
openScheduleDetails: (scheduleId: string) => void;
|
||||
}): TableColumn => {
|
||||
return {
|
||||
field: 'name',
|
||||
name: i18n.COLUMN_NAME,
|
||||
render: (_, schedule: AttackDiscoverySchedule) => (
|
||||
<Name schedule={schedule} openScheduleDetails={openScheduleDetails} />
|
||||
),
|
||||
truncateText: true,
|
||||
width: '60%',
|
||||
align: 'left',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { EuiTableFieldDataColumnType } from '@elastic/eui';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { createStatusColumn } from './status';
|
||||
import { TestProviders } from '../../../../../../common/mock';
|
||||
import { mockAttackDiscoverySchedule } from '../../../../mock/mock_attack_discovery_schedule';
|
||||
|
||||
describe('Status Column', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const column = createStatusColumn() as EuiTableFieldDataColumnType<AttackDiscoverySchedule>;
|
||||
|
||||
render(<TestProviders>{column.render?.('', mockAttackDiscoverySchedule)}</TestProviders>);
|
||||
});
|
||||
|
||||
it('should render schedule execution status', () => {
|
||||
expect(screen.getByTestId('scheduleExecutionStatus')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
import * as i18n from './translations';
|
||||
import type { TableColumn } from './constants';
|
||||
import { StatusBadge } from './status_badge';
|
||||
|
||||
export const createStatusColumn = (): TableColumn => {
|
||||
return {
|
||||
field: 'status',
|
||||
name: i18n.COLUMN_STATUS,
|
||||
render: (_, schedule: AttackDiscoverySchedule) => <StatusBadge schedule={schedule} />,
|
||||
truncateText: true,
|
||||
width: '100px',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { StatusBadge } from '.';
|
||||
import { TestProviders } from '../../../../../../../common/mock';
|
||||
import { mockAttackDiscoverySchedule } from '../../../../../mock/mock_attack_discovery_schedule';
|
||||
import { waitForEuiToolTipVisible } from '@elastic/eui/lib/test/rtl';
|
||||
|
||||
const renderScheduleStatus = (
|
||||
status: 'unknown' | 'ok' | 'active' | 'error' | 'warning' = 'ok',
|
||||
message?: string
|
||||
) => {
|
||||
const lastExecution = {
|
||||
date: new Date().toISOString(),
|
||||
status,
|
||||
duration: 26,
|
||||
message,
|
||||
};
|
||||
render(
|
||||
<TestProviders>
|
||||
<StatusBadge schedule={{ ...mockAttackDiscoverySchedule, lastExecution }} />
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('StatusBadge', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render schedule execution status', () => {
|
||||
renderScheduleStatus();
|
||||
expect(screen.getByTestId('scheduleExecutionStatus')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct label for `ok` status', () => {
|
||||
renderScheduleStatus('ok');
|
||||
expect(screen.getByTestId('scheduleExecutionStatus')).toHaveTextContent('Success');
|
||||
});
|
||||
|
||||
it('should render correct label for `active` status', () => {
|
||||
renderScheduleStatus('active');
|
||||
expect(screen.getByTestId('scheduleExecutionStatus')).toHaveTextContent('Success');
|
||||
});
|
||||
|
||||
it('should render correct label for `error` status', () => {
|
||||
renderScheduleStatus('error');
|
||||
expect(screen.getByTestId('scheduleExecutionStatus')).toHaveTextContent('Failed');
|
||||
});
|
||||
|
||||
it('should render correct label for `warning` status', () => {
|
||||
renderScheduleStatus('warning');
|
||||
expect(screen.getByTestId('scheduleExecutionStatus')).toHaveTextContent('Warning');
|
||||
});
|
||||
|
||||
it('should render correct label for `unknown` status', () => {
|
||||
renderScheduleStatus('unknown');
|
||||
expect(screen.getByTestId('scheduleExecutionStatus')).toHaveTextContent('Unknown');
|
||||
});
|
||||
|
||||
it('should render execution status as a tooltip if execution message is not set', async () => {
|
||||
renderScheduleStatus('error');
|
||||
|
||||
const status = screen.getByTestId('scheduleExecutionStatus');
|
||||
fireEvent.mouseOver(status.parentElement as Node);
|
||||
await waitForEuiToolTipVisible();
|
||||
|
||||
const tooltip = screen.getByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('Failed');
|
||||
});
|
||||
|
||||
it('should render existing execution message as a tooltip', async () => {
|
||||
renderScheduleStatus('error', 'Failed badly!');
|
||||
|
||||
const status = screen.getByTestId('scheduleExecutionStatus');
|
||||
fireEvent.mouseOver(status.parentElement as Node);
|
||||
await waitForEuiToolTipVisible();
|
||||
|
||||
const tooltip = screen.getByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('Failed badly!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { EuiThemeComputed } from '@elastic/eui';
|
||||
import { EuiHealth, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import type {
|
||||
AttackDiscoverySchedule,
|
||||
AttackDiscoveryScheduleExecutionStatus,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const statusTextWrapperClassName = css`
|
||||
width: 100%;
|
||||
display: inline-grid;
|
||||
`;
|
||||
|
||||
const getExecutionStatusHealthColor = (
|
||||
status: AttackDiscoveryScheduleExecutionStatus,
|
||||
euiTheme: EuiThemeComputed
|
||||
) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
case 'ok':
|
||||
return euiTheme.colors.success;
|
||||
case 'error':
|
||||
return euiTheme.colors.danger;
|
||||
case 'warning':
|
||||
return euiTheme.colors.warning;
|
||||
default:
|
||||
return 'subdued';
|
||||
}
|
||||
};
|
||||
|
||||
const getExecutionStatusLabel = (status: AttackDiscoveryScheduleExecutionStatus) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
case 'ok':
|
||||
return i18n.STATUS_SUCCESS;
|
||||
case 'error':
|
||||
return i18n.STATUS_FAILED;
|
||||
case 'warning':
|
||||
return i18n.STATUS_WARNING;
|
||||
default:
|
||||
return i18n.STATUS_UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
interface StatusBadgeProps {
|
||||
schedule: AttackDiscoverySchedule;
|
||||
}
|
||||
|
||||
export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(({ schedule }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
if (!schedule.lastExecution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const executionStatus = schedule.lastExecution.status;
|
||||
const executionMessage = schedule.lastExecution.message;
|
||||
const label = getExecutionStatusLabel(executionStatus);
|
||||
const color = getExecutionStatusHealthColor(executionStatus, euiTheme);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={executionMessage ?? label}>
|
||||
<EuiHealth color={color} data-test-subj={'scheduleExecutionStatus'}>
|
||||
<div className={statusTextWrapperClassName}>
|
||||
<span className="eui-textTruncate">{label}</span>
|
||||
</div>
|
||||
</EuiHealth>
|
||||
</EuiToolTip>
|
||||
);
|
||||
});
|
||||
StatusBadge.displayName = 'StatusBadge';
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const STATUS_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.successLabel',
|
||||
{
|
||||
defaultMessage: 'Success',
|
||||
}
|
||||
);
|
||||
|
||||
export const STATUS_FAILED = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.failedLabel',
|
||||
{
|
||||
defaultMessage: 'Failed',
|
||||
}
|
||||
);
|
||||
|
||||
export const STATUS_WARNING = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.warningLabel',
|
||||
{
|
||||
defaultMessage: 'Warning',
|
||||
}
|
||||
);
|
||||
|
||||
export const STATUS_UNKNOWN = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.status.unknownLabel',
|
||||
{
|
||||
defaultMessage: 'Unknown',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const COLUMN_ACTIONS = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.actionsLabel',
|
||||
{
|
||||
defaultMessage: 'Actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_ACTIONS_BUTTON_ARIAL_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.deleteActionsArialLabel',
|
||||
{
|
||||
defaultMessage: 'Delete schedule',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_ENABLE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.enableHeaderLabel',
|
||||
{
|
||||
defaultMessage: 'Enabled',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_NAME = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.nameHeaderLabel',
|
||||
{
|
||||
defaultMessage: 'Title',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_STATUS = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.tableColumn.statusHeaderLabel',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { SchedulesTable } from '.';
|
||||
import { useFindAttackDiscoverySchedules } from '../logic/use_find_schedules';
|
||||
import { useEnableAttackDiscoverySchedule } from '../logic/use_enable_schedule';
|
||||
import { useDisableAttackDiscoverySchedule } from '../logic/use_disable_schedule';
|
||||
import { useDeleteAttackDiscoverySchedule } from '../logic/use_delete_schedule';
|
||||
import { mockFindAttackDiscoverySchedules } from '../../../mock/mock_find_attack_discovery_schedules';
|
||||
|
||||
jest.mock('../logic/use_find_schedules');
|
||||
jest.mock('../logic/use_enable_schedule');
|
||||
jest.mock('../logic/use_disable_schedule');
|
||||
jest.mock('../logic/use_delete_schedule');
|
||||
|
||||
const mockUseFindAttackDiscoverySchedules = useFindAttackDiscoverySchedules as jest.MockedFunction<
|
||||
typeof useFindAttackDiscoverySchedules
|
||||
>;
|
||||
|
||||
const enableAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseEnableAttackDiscoverySchedule =
|
||||
useEnableAttackDiscoverySchedule as jest.MockedFunction<typeof useEnableAttackDiscoverySchedule>;
|
||||
const disableAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseDisableAttackDiscoverySchedule =
|
||||
useDisableAttackDiscoverySchedule as jest.MockedFunction<
|
||||
typeof useDisableAttackDiscoverySchedule
|
||||
>;
|
||||
const deleteAttackDiscoveryScheduleMock = jest.fn();
|
||||
const mockUseDeleteAttackDiscoverySchedule =
|
||||
useDeleteAttackDiscoverySchedule as jest.MockedFunction<typeof useDeleteAttackDiscoverySchedule>;
|
||||
|
||||
describe('SchedulesTable', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseFindAttackDiscoverySchedules.mockReturnValue({
|
||||
data: mockFindAttackDiscoverySchedules,
|
||||
isLoading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useFindAttackDiscoverySchedules>>);
|
||||
|
||||
mockUseEnableAttackDiscoverySchedule.mockReturnValue({
|
||||
mutateAsync: enableAttackDiscoveryScheduleMock,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useEnableAttackDiscoverySchedule>>);
|
||||
mockUseDisableAttackDiscoverySchedule.mockReturnValue({
|
||||
mutateAsync: disableAttackDiscoveryScheduleMock,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useDisableAttackDiscoverySchedule>>);
|
||||
mockUseDeleteAttackDiscoverySchedule.mockReturnValue({
|
||||
mutateAsync: deleteAttackDiscoveryScheduleMock,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useDeleteAttackDiscoverySchedule>>);
|
||||
});
|
||||
|
||||
it('should render the schedules table container', () => {
|
||||
const { getByTestId } = render(<SchedulesTable />);
|
||||
|
||||
expect(getByTestId('schedulesTableContainer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the schedules table description', () => {
|
||||
const { getByTestId } = render(<SchedulesTable />);
|
||||
|
||||
expect(getByTestId('schedulesTableDescription')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the correct number of rows in the schedules table', () => {
|
||||
const { getAllByRole } = render(<SchedulesTable />);
|
||||
|
||||
expect(getAllByRole('row').length).toBe(1 + mockFindAttackDiscoverySchedules.schedules.length); // 1 header row + schedule rows
|
||||
});
|
||||
|
||||
it('should invoke delete schedule mutation', async () => {
|
||||
const { getAllByTestId } = render(<SchedulesTable />);
|
||||
|
||||
const firstDeleteButton = getAllByTestId('deleteButton')[0];
|
||||
act(() => {
|
||||
fireEvent.click(firstDeleteButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteAttackDiscoveryScheduleMock).toHaveBeenCalledWith({
|
||||
id: mockFindAttackDiscoverySchedules.schedules[0].id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke disable schedule mutation', async () => {
|
||||
const { getAllByTestId } = render(<SchedulesTable />);
|
||||
|
||||
const firstSwitchButton = getAllByTestId('scheduleSwitch')[0];
|
||||
act(() => {
|
||||
fireEvent.click(firstSwitchButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(disableAttackDiscoveryScheduleMock).toHaveBeenCalledWith({
|
||||
id: mockFindAttackDiscoverySchedules.schedules[0].id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke enable schedule mutation', async () => {
|
||||
const schedules = [
|
||||
mockFindAttackDiscoverySchedules.schedules[0],
|
||||
{ ...mockFindAttackDiscoverySchedules.schedules[1], enabled: false },
|
||||
];
|
||||
mockUseFindAttackDiscoverySchedules.mockReturnValue({
|
||||
data: { total: schedules.length, schedules },
|
||||
isLoading: false,
|
||||
} as unknown as jest.Mocked<ReturnType<typeof useFindAttackDiscoverySchedules>>);
|
||||
|
||||
const { getAllByTestId } = render(<SchedulesTable />);
|
||||
|
||||
const secondSwitchButton = getAllByTestId('scheduleSwitch')[1];
|
||||
act(() => {
|
||||
fireEvent.click(secondSwitchButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(enableAttackDiscoveryScheduleMock).toHaveBeenCalledWith({
|
||||
id: schedules[1].id,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CriteriaWithPagination } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiBasicTable } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { AttackDiscoverySchedule } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { useColumns } from './use_columns';
|
||||
import { useFindAttackDiscoverySchedules } from '../logic/use_find_schedules';
|
||||
import { useEnableAttackDiscoverySchedule } from '../logic/use_enable_schedule';
|
||||
import { useDisableAttackDiscoverySchedule } from '../logic/use_disable_schedule';
|
||||
import { useDeleteAttackDiscoverySchedule } from '../logic/use_delete_schedule';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT_FIELD = 'name';
|
||||
const DEFAULT_SORT_DIRECTION = 'asc';
|
||||
|
||||
/**
|
||||
* Table Component for displaying Attack Discovery Schedules
|
||||
*/
|
||||
export const SchedulesTable: React.FC = React.memo(() => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const [sortField, setSortField] = useState<keyof AttackDiscoverySchedule>(DEFAULT_SORT_FIELD);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION);
|
||||
|
||||
const { data: { schedules, total } = { schedules: [], total: 0 }, isLoading: isDataLoading } =
|
||||
useFindAttackDiscoverySchedules({
|
||||
page: pageIndex,
|
||||
perPage: pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
});
|
||||
|
||||
const pagination = useMemo(() => {
|
||||
return {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: total,
|
||||
};
|
||||
}, [pageIndex, pageSize, total]);
|
||||
|
||||
const sorting = useMemo(() => {
|
||||
return {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
}, [sortDirection, sortField]);
|
||||
|
||||
const onTableChange = useCallback(
|
||||
({ page, sort }: CriteriaWithPagination<AttackDiscoverySchedule>) => {
|
||||
if (page) {
|
||||
setPageIndex(page.index);
|
||||
setPageSize(page.size);
|
||||
}
|
||||
if (sort) {
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const [isTableLoading, setTableLoading] = useState(false);
|
||||
|
||||
const { mutateAsync: enableAttackDiscoverySchedule } = useEnableAttackDiscoverySchedule();
|
||||
const { mutateAsync: disableAttackDiscoverySchedule } = useDisableAttackDiscoverySchedule();
|
||||
const { mutateAsync: deleteAttackDiscoverySchedule } = useDeleteAttackDiscoverySchedule();
|
||||
|
||||
const openScheduleDetails = useCallback((scheduleId: string) => {
|
||||
// TODO: implement attack discovery schedule details
|
||||
}, []);
|
||||
const enableSchedule = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
setTableLoading(true);
|
||||
await enableAttackDiscoverySchedule({ id });
|
||||
} catch (err) {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
setTableLoading(false);
|
||||
}
|
||||
},
|
||||
[enableAttackDiscoverySchedule]
|
||||
);
|
||||
const disableSchedule = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
setTableLoading(true);
|
||||
await disableAttackDiscoverySchedule({ id });
|
||||
} catch (err) {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
setTableLoading(false);
|
||||
}
|
||||
},
|
||||
[disableAttackDiscoverySchedule]
|
||||
);
|
||||
const deleteSchedule = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
setTableLoading(true);
|
||||
await deleteAttackDiscoverySchedule({ id });
|
||||
} catch (err) {
|
||||
// Error is handled by the mutation's onError callback, so no need to do anything here
|
||||
} finally {
|
||||
setTableLoading(false);
|
||||
}
|
||||
},
|
||||
[deleteAttackDiscoverySchedule]
|
||||
);
|
||||
|
||||
const rulesColumns = useColumns({
|
||||
isDisabled: isDataLoading,
|
||||
isLoading: isTableLoading,
|
||||
openScheduleDetails,
|
||||
enableSchedule,
|
||||
disableSchedule,
|
||||
deleteSchedule,
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-test-subj="schedulesTableContainer">
|
||||
<div data-test-subj="schedulesTableDescription">
|
||||
{i18n.ATTACK_DISCOVER_SCHEDULES_DESCRIPTION}
|
||||
</div>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiBasicTable<AttackDiscoverySchedule>
|
||||
loading={isTableLoading}
|
||||
items={schedules}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
onChange={onTableChange}
|
||||
itemId={'id'}
|
||||
data-test-subj={'schedulesTable'}
|
||||
columns={rulesColumns}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SchedulesTable.displayName = 'SchedulesTable';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_DISCOVER_SCHEDULES_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.schedule.table.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Scheduled Attack Discoveries will generate automatically, based on their settings.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useColumns } from './use_columns';
|
||||
import {
|
||||
createActionsColumn,
|
||||
createEnableColumn,
|
||||
createNameColumn,
|
||||
createStatusColumn,
|
||||
} from './columns';
|
||||
|
||||
jest.mock('./columns');
|
||||
|
||||
const mockCreateActionsColumn = createActionsColumn as jest.MockedFunction<
|
||||
typeof createActionsColumn
|
||||
>;
|
||||
const mockCreateEnableColumn = createEnableColumn as jest.MockedFunction<typeof createEnableColumn>;
|
||||
const mockCreateNameColumn = createNameColumn as jest.MockedFunction<typeof createNameColumn>;
|
||||
const mockCreateStatusColumn = createStatusColumn as jest.MockedFunction<typeof createStatusColumn>;
|
||||
|
||||
const openScheduleDetails = jest.fn();
|
||||
const enableSchedule = jest.fn();
|
||||
const disableSchedule = jest.fn();
|
||||
const deleteSchedule = jest.fn();
|
||||
|
||||
describe('useColumns', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
renderHook(() =>
|
||||
useColumns({
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
openScheduleDetails,
|
||||
enableSchedule,
|
||||
disableSchedule,
|
||||
deleteSchedule,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should invoke `createNameColumn`', () => {
|
||||
expect(mockCreateNameColumn).toHaveBeenCalledWith({ openScheduleDetails });
|
||||
});
|
||||
|
||||
it('should invoke `createStatusColumn`', () => {
|
||||
expect(mockCreateStatusColumn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should invoke `createEnableColumn`', () => {
|
||||
expect(mockCreateEnableColumn).toHaveBeenCalledWith({
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
onSwitchChange: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should invoke `createActionsColumn`', () => {
|
||||
expect(mockCreateActionsColumn).toHaveBeenCalledWith({ isDisabled: false, deleteSchedule });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { TableColumn } from './columns';
|
||||
import {
|
||||
createActionsColumn,
|
||||
createEnableColumn,
|
||||
createNameColumn,
|
||||
createStatusColumn,
|
||||
} from './columns';
|
||||
|
||||
export const useColumns = ({
|
||||
isDisabled,
|
||||
isLoading,
|
||||
openScheduleDetails,
|
||||
enableSchedule,
|
||||
disableSchedule,
|
||||
deleteSchedule,
|
||||
}: {
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
openScheduleDetails: (scheduleId: string) => void;
|
||||
enableSchedule: (scheduleId: string) => Promise<void>;
|
||||
disableSchedule: (scheduleId: string) => Promise<void>;
|
||||
deleteSchedule: (scheduleId: string) => Promise<void>;
|
||||
}): TableColumn[] => {
|
||||
const onSwitchChange = useCallback(
|
||||
async (scheduleId: string, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
await enableSchedule(scheduleId);
|
||||
} else {
|
||||
await disableSchedule(scheduleId);
|
||||
}
|
||||
},
|
||||
[disableSchedule, enableSchedule]
|
||||
);
|
||||
return useMemo(
|
||||
() => [
|
||||
createNameColumn({ openScheduleDetails }),
|
||||
createStatusColumn(),
|
||||
createEnableColumn({ isDisabled, isLoading, onSwitchChange }),
|
||||
createActionsColumn({ isDisabled, deleteSchedule }),
|
||||
],
|
||||
[deleteSchedule, isDisabled, isLoading, onSwitchChange, openScheduleDetails]
|
||||
);
|
||||
};
|
|
@ -12,7 +12,11 @@ import type {
|
|||
GenerationInterval,
|
||||
AttackDiscoveryStats,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { AttackDiscoveryPostResponse, API_VERSIONS } from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
AttackDiscoveryPostResponse,
|
||||
API_VERSIONS,
|
||||
ATTACK_DISCOVERY,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFetchAnonymizationFields } from '@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields';
|
||||
|
@ -188,7 +192,7 @@ export const useAttackDiscovery = ({
|
|||
setApproximateFutureTime(null);
|
||||
|
||||
// call the internal API to generate attack discoveries:
|
||||
const rawResponse = await http.post('/internal/elastic_assistant/attack_discovery', {
|
||||
const rawResponse = await http.post(ATTACK_DISCOVERY, {
|
||||
body: JSON.stringify(bodyWithOverrides),
|
||||
version: API_VERSIONS.internal.v1,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue