mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
Merge branch '8.19' into update-bundled-packages-20250625161128
This commit is contained in:
commit
2cf151312d
281 changed files with 7510 additions and 1064 deletions
|
@ -30,13 +30,13 @@ disabled:
|
|||
- x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts
|
||||
- x-pack/platform/test/alerting_api_integration/spaces_only_legacy/config.ts
|
||||
- x-pack/test/cloud_integration/config.ts
|
||||
- x-pack/test/load/config.ts
|
||||
- x-pack/test/plugin_api_perf/config.js
|
||||
- x-pack/platform/test/load/config.ts
|
||||
- x-pack/platform/test/plugin_api_perf/config.ts
|
||||
- x-pack/test/screenshot_creation/config.ts
|
||||
- x-pack/test/fleet_packages/config.ts
|
||||
- x-pack/platform/test/fleet_packages/config.ts
|
||||
|
||||
# Scalability testing config that we run in its own pipeline
|
||||
- x-pack/test/scalability/config.ts
|
||||
- x-pack/platform/test/scalability/config.ts
|
||||
|
||||
# Cypress configs, for now these are still run manually
|
||||
- x-pack/test/fleet_cypress/cli_config.ts
|
||||
|
@ -171,7 +171,7 @@ enabled:
|
|||
- x-pack/test/fleet_api_integration/config.package_policy.ts
|
||||
- x-pack/test/fleet_api_integration/config.space_awareness.ts
|
||||
- x-pack/test/fleet_functional/config.ts
|
||||
- x-pack/test/ftr_apis/security_and_spaces/config.ts
|
||||
- x-pack/platform/test/ftr_apis/security_and_spaces/config.ts
|
||||
- x-pack/test/functional_basic/apps/ml/permissions/config.ts
|
||||
- x-pack/test/functional_basic/apps/ml/data_visualizer/group1/config.ts
|
||||
- x-pack/test/functional_basic/apps/ml/data_visualizer/group2/config.ts
|
||||
|
@ -327,7 +327,7 @@ enabled:
|
|||
- x-pack/test/spaces_api_integration/security_and_spaces/config_trial.ts
|
||||
- x-pack/test/spaces_api_integration/security_and_spaces/copy_to_space_config_trial.ts
|
||||
- x-pack/test/spaces_api_integration/spaces_only/config.ts
|
||||
- x-pack/test/task_manager_claimer_update_by_query/config.ts
|
||||
- x-pack/platform/test/task_manager_claimer_update_by_query/config.ts
|
||||
- x-pack/test/ui_capabilities/security_and_spaces/config.ts
|
||||
- x-pack/test/ui_capabilities/spaces_only/config.ts
|
||||
- x-pack/test/upgrade_assistant_integration/config.ts
|
||||
|
|
|
@ -197,13 +197,14 @@ const getPipeline = (filename: string, removeSteps = true) => {
|
|||
}
|
||||
|
||||
if (
|
||||
(await doAnyChangesMatch([
|
||||
((await doAnyChangesMatch([
|
||||
/\.docnav\.json$/,
|
||||
/\.apidocs\.json$/,
|
||||
/\.devdocs\.json$/,
|
||||
/\.mdx$/,
|
||||
/^dev_docs\/.*(png|gif|jpg|jpeg|webp)$/,
|
||||
])) ||
|
||||
])) &&
|
||||
process.env.GITHUB_PR_TARGET_BRANCH === 'main') ||
|
||||
GITHUB_PR_LABELS.includes('ci:build-next-docs')
|
||||
) {
|
||||
pipeline.push(getPipeline('.buildkite/pipelines/pull_request/check_next_docs.yml'));
|
||||
|
|
|
@ -9,4 +9,4 @@ echo '--- Installing all packages'
|
|||
node scripts/functional_tests \
|
||||
--debug \
|
||||
--bail \
|
||||
--config x-pack/test/fleet_packages/config.ts
|
||||
--config x-pack/platform/test/fleet_packages/config.ts
|
||||
|
|
|
@ -22,7 +22,7 @@ checkout_and_compile_load_runner
|
|||
|
||||
echo "--- Run single apis capacity tests"
|
||||
cd "$KIBANA_DIR"
|
||||
node scripts/run_scalability --kibana-install-dir "$KIBANA_BUILD_LOCATION" --journey-path "x-pack/test/scalability/apis"
|
||||
node scripts/run_scalability --kibana-install-dir "$KIBANA_BUILD_LOCATION" --journey-path "x-pack/platform/test/scalability/apis"
|
||||
|
||||
echo "--- Upload test results"
|
||||
upload_test_results
|
||||
|
|
25
.github/CODEOWNERS
vendored
25
.github/CODEOWNERS
vendored
|
@ -796,7 +796,7 @@ src/platform/packages/shared/kbn-safer-lodash-set @elastic/kibana-security
|
|||
x-pack/test/security_api_integration/plugins/saml_provider @elastic/kibana-security
|
||||
x-pack/platform/packages/shared/kbn-sample-parser @elastic/streams-program-team
|
||||
x-pack/platform/test/plugin_api_integration/plugins/sample_task_plugin @elastic/response-ops
|
||||
x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget @elastic/response-ops
|
||||
x-pack/platform/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget @elastic/response-ops
|
||||
src/platform/test/plugin_functional/plugins/saved_object_export_transforms @elastic/kibana-core
|
||||
src/platform/test/plugin_functional/plugins/saved_object_import_warnings @elastic/kibana-core
|
||||
x-pack/platform/test/saved_object_api_integration/common/plugins/saved_object_test_plugin @elastic/kibana-security
|
||||
|
@ -989,7 +989,7 @@ x-pack/solutions/observability/plugins/synthetics @elastic/obs-ux-management-tea
|
|||
x-pack/packages/kbn-synthetics-private-location @elastic/obs-ux-management-team
|
||||
x-pack/platform/plugins/private/task_manager_dependencies @elastic/response-ops
|
||||
x-pack/platform/test/alerting_api_integration/common/plugins/task_manager_fixture @elastic/response-ops
|
||||
x-pack/test/plugin_api_perf/plugins/task_manager_performance @elastic/response-ops
|
||||
x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance @elastic/response-ops
|
||||
x-pack/platform/plugins/shared/task_manager @elastic/response-ops
|
||||
src/platform/packages/shared/kbn-telemetry @elastic/kibana-core @elastic/obs-ai-assistant
|
||||
src/platform/plugins/shared/telemetry_collection_manager @elastic/kibana-core
|
||||
|
@ -1392,7 +1392,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
|
|||
/x-pack/test/functional/es_archives/fleet @elastic/fleet
|
||||
/x-pack/test/api_integration/services/fleet_and_agents.ts @elastic/fleet
|
||||
/x-pack/test/fleet_api_integration @elastic/fleet
|
||||
/x-pack/test/fleet_packages @elastic/fleet
|
||||
/x-pack/platform/test/fleet_packages @elastic/fleet
|
||||
^/src/platform/test/api_integration/apis/custom_integration/*.ts @elastic/fleet
|
||||
/x-pack/test/fleet_cypress @elastic/fleet
|
||||
/x-pack/test/fleet_functional @elastic/fleet
|
||||
|
@ -1609,6 +1609,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security
|
|||
# QA - Appex QA
|
||||
.buildkite/scout_ci_config.yml @elastic/appex-qa
|
||||
/x-pack/test/.gitignore @elastic/appex-qa
|
||||
/x-pack/platform/test/index.d.ts @elastic/appex-qa
|
||||
/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/es/roles.yml @elastic/appex-qa
|
||||
/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/oblt/roles.yml @elastic/appex-qa
|
||||
/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml @elastic/appex-qa
|
||||
|
@ -1676,7 +1677,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security
|
|||
/x-pack/platform/test/api_integration/services @elastic/appex-qa
|
||||
/x-pack/platform/test/api_integration/apis/kibana/config.ts @elastic/appex-qa
|
||||
/x-pack/test/tsconfig.json @elastic/appex-qa
|
||||
/x-pack/test/load @elastic/appex-qa
|
||||
/x-pack/platform/test/load @elastic/appex-qa
|
||||
^/src/platform/test/tsconfig.json @elastic/appex-qa
|
||||
^/src/platform/test/plugin_functional/services/index.ts @elastic/appex-qa
|
||||
^/src/platform/test/plugin_functional/README.md @elastic/appex-qa
|
||||
|
@ -1727,7 +1728,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security
|
|||
^/src/platform/test/accessibility/ftr_provider_context.ts @elastic/appex-qa
|
||||
^/src/platform/test/accessibility/config.ts @elastic/appex-qa
|
||||
^/src/platform/test/accessibility/apps/index.ts @elastic/appex-qa
|
||||
/x-pack/test/scalability @elastic/appex-qa
|
||||
/x-pack/platform/test/scalability @elastic/appex-qa
|
||||
/src/dev/performance @elastic/appex-qa
|
||||
/x-pack/test/functional/config.*.* @elastic/appex-qa
|
||||
/x-pack/platform/test/functional/config.*.* @elastic/appex-qa
|
||||
|
@ -1923,9 +1924,9 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi
|
|||
/x-pack/test/functional/es_archives/security @elastic/kibana-security
|
||||
/x-pack/test/functional/fixtures/kbn_archiver/spaces @elastic/kibana-security
|
||||
/x-pack/test/functional/fixtures/kbn_archiver/security @elastic/kibana-security
|
||||
/x-pack/test/ftr_apis/common/lib @elastic/kibana-security
|
||||
/x-pack/test/ftr_apis/common/fixtures/es_archiver/base_data/space_1.json @elastic/kibana-security # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/ftr_apis/security_and_spaces/apis/test_utils.ts#L33
|
||||
/x-pack/test/ftr_apis/common/fixtures/es_archiver/base_data/default_space.json @elastic/kibana-security # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/test/ftr_apis/security_and_spaces/apis/test_utils.ts#L33
|
||||
/x-pack/platform/test/ftr_apis/common/lib @elastic/kibana-security
|
||||
/x-pack/platform/test/ftr_apis/common/fixtures/es_archiver/base_data/space_1.json @elastic/kibana-security # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/platform/test/ftr_apis/security_and_spaces/apis/test_utils.ts#L33
|
||||
/x-pack/platform/test/ftr_apis/common/fixtures/es_archiver/base_data/default_space.json @elastic/kibana-security # Assigned per only use: https://github.com/elastic/kibana/blob/main/x-pack/platform/test/ftr_apis/security_and_spaces/apis/test_utils.ts#L33
|
||||
/x-pack/platform/test/api_integration/apis/cloud @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/198444
|
||||
^/src/platform/test/plugin_functional/snapshots/baseline/hardening @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/190716
|
||||
^/src/platform/test/functional/page_objects/login_page.ts @elastic/kibana-security
|
||||
|
@ -1942,7 +1943,7 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi
|
|||
/x-pack/test/common/services/spaces.ts @elastic/kibana-security
|
||||
/x-pack/test/api_integration/config_security_*.ts @elastic/kibana-security
|
||||
/x-pack/test/functional/apps/api_keys @elastic/kibana-security
|
||||
/x-pack/test/ftr_apis/security_and_spaces @elastic/kibana-security
|
||||
/x-pack/platform//test/ftr_apis/security_and_spaces @elastic/kibana-security
|
||||
^/src/platform/test/server_integration/services/supertest.js @elastic/kibana-security @elastic/kibana-core
|
||||
^/src/platform/test/server_integration/http/ssl @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/53810
|
||||
^/src/platform/test/server_integration/http/ssl_with_p12 @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/199795#discussion_r1846522206
|
||||
|
@ -2000,8 +2001,8 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi
|
|||
# Response Ops team
|
||||
/x-pack/test/functional/es_archives/rule_registry @elastic/response-ops
|
||||
/x-pack/test/functional/es_archives/event_log_multiple_indicies @elastic/response-ops
|
||||
/x-pack/test/functional/es_archives/task_manager* @elastic/response-ops # Assigned per https://github.com/elastic/kibana/blob/assign-response-ops/x-pack/test/plugin_api_perf/plugins/task_manager_performance/kibana.jsonc#L4
|
||||
/x-pack/test/plugin_api_perf @elastic/response-ops # Assigned per https://github.com/elastic/kibana/blob/assign-response-ops/x-pack/test/plugin_api_perf/plugins/task_manager_performance/kibana.jsonc#L4
|
||||
/x-pack/test/functional/es_archives/task_manager* @elastic/response-ops # Assigned per https://github.com/elastic/kibana/blob/assign-response-ops/x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance/kibana.jsonc#L4
|
||||
/x-pack/platform/test/plugin_api_perf @elastic/response-ops # Assigned per https://github.com/elastic/kibana/blob/assign-response-ops/x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance/kibana.jsonc#L4
|
||||
/x-pack/test/functional/page_objects/maintenance_windows_page.ts @elastic/response-ops
|
||||
/x-pack/test_serverless/functional/test_suites/observability/screenshot_creation/index.ts @elastic/response-ops
|
||||
/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts @elastic/response-ops
|
||||
|
@ -2036,7 +2037,7 @@ x-pack/platform/plugins/private/cloud_integrations/cloud_full_story/server/confi
|
|||
/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/ @elastic/response-ops
|
||||
/x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/ @elastic/response-ops
|
||||
/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/response-ops
|
||||
/x-pack/test/task_manager_claimer_update_by_query/ @elastic/response-ops
|
||||
/x-pack/platform/test/task_manager_claimer_update_by_query/ @elastic/response-ops
|
||||
/docs/user/alerting/ @elastic/response-ops
|
||||
/docs/management/connectors/ @elastic/response-ops
|
||||
/x-pack/test/cases_api_integration/ @elastic/response-ops
|
||||
|
|
|
@ -8,6 +8,7 @@ tags: ['kibana', 'onboarding', 'setup', 'performance', 'development', 'telemetry
|
|||
---
|
||||
|
||||
## Overview
|
||||
|
||||
It is important to test individual API endpoint for the baseline performance, scalability, or breaking point. If an API doesn’t meet performance requirements, it is a bottleneck.
|
||||
This capacity tests track how response time changes while we slowly increase number of concurrent requests per second.
|
||||
While using similar load model, we are able to identify how many requests per second each endpoint can hold with response time staying below critical threshold.
|
||||
|
@ -15,6 +16,7 @@ While using similar load model, we are able to identify how many requests per se
|
|||
Capacity API test defines 3 response time thresholds (default ones: 3000, 6000, 12000) in ms. Test results report rps (requests per second) for each threshold.
|
||||
|
||||
Test results are reported using EBT in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"_index": "backing-kibana-server-scalability-metrics-000003",
|
||||
|
@ -37,7 +39,9 @@ Test results are reported using EBT in the following format:
|
|||
```
|
||||
|
||||
### Adding a new test
|
||||
Create a new json file in `x-pack/test/scalability/apis` with required properties:
|
||||
|
||||
Create a new json file in `x-pack/platform/test/scalability/apis` with required properties:
|
||||
|
||||
- **journeyName** is a test name, e.g. `GET /internal/security/session`
|
||||
- **scalabilitySetup** is used to set load model
|
||||
- **testData** is used to populate Elasticsearch and Kibana wth test data
|
||||
|
@ -47,13 +51,15 @@ Create a new json file in `x-pack/test/scalability/apis` with required propertie
|
|||
Warmup phase simulates 10 concurrent requests during 30s period and is important to get consistent results in test phase.
|
||||
Test phase simulates increasing concurrent requests from `minUsersCount` to `maxUsersCount` within `duration` time.
|
||||
Both `maxUsersCount` and `duration` in test phase should be adjusted for individual endpoint:
|
||||
- `maxUsersCount` should be reasonable and enough to reach endpoint limits
|
||||
- `duration` should be long enough to ramp up requests with low pace (1-2 requests per second)
|
||||
|
||||
- `maxUsersCount` should be reasonable and enough to reach endpoint limits
|
||||
- `duration` should be long enough to ramp up requests with low pace (1-2 requests per second)
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"journeyName": "GET /internal/security/session",
|
||||
"journeyName": "GET /internal/security/session",
|
||||
"scalabilitySetup": {
|
||||
"warmup": [
|
||||
{
|
||||
|
@ -99,6 +105,7 @@ Example:
|
|||
```
|
||||
|
||||
Override default response time thresholds by adding to `scalabilitySetup`:
|
||||
|
||||
```json
|
||||
"responseTimeThreshold": {
|
||||
"threshold1": 1000,
|
||||
|
@ -108,14 +115,16 @@ Override default response time thresholds by adding to `scalabilitySetup`:
|
|||
```
|
||||
|
||||
### Running api capacity journey locally
|
||||
|
||||
Clone [kibana-load-testing](https://github.com/elastic/kibana-load-testing) repo.
|
||||
|
||||
Use the Node script from kibana root directory:
|
||||
`node scripts/run_scalability_cli.js --journey-path x-pack/test/scalability/apis/$YOUR_JOURNEY_NAME.ts`
|
||||
`node scripts/run_scalability_cli.js --journey-path x-pack/platform/test/scalability/apis/$YOUR_JOURNEY_NAME.ts`
|
||||
|
||||
Use `--kibana-install-dir` flag to test build
|
||||
|
||||
### Benchmarking performance on CI
|
||||
|
||||
In order to keep track on performance metrics stability, api capacity tests are run on main branch with a scheduled interval.
|
||||
Bare metal machine is used to produce results as stable and reproducible as possible.
|
||||
|
||||
|
@ -128,6 +137,7 @@ RAM: 128 GB
|
|||
SSD: 1.92 TB Data center Gen4 NVMe
|
||||
|
||||
#### Track performance results
|
||||
|
||||
APM metrics are reported to [kibana-stats](https://kibana-stats.elastic.dev/) cluster.
|
||||
You can filter transactions using labels, e.g. `labels.journeyName : "GET /internal/security/session"`
|
||||
|
||||
|
|
|
@ -349,6 +349,7 @@ paths:
|
|||
- $ref: '#/components/schemas/jira_config'
|
||||
- $ref: '#/components/schemas/genai_azure_config'
|
||||
- $ref: '#/components/schemas/genai_openai_config'
|
||||
- $ref: '#/components/schemas/genai_openai_other_config'
|
||||
- $ref: '#/components/schemas/opsgenie_config'
|
||||
- $ref: '#/components/schemas/pagerduty_config'
|
||||
- $ref: '#/components/schemas/sentinelone_config'
|
||||
|
@ -65454,12 +65455,30 @@ components:
|
|||
The URL of the incoming webhook. If you are using the `xpack.actions.allowedHosts` setting, add the hostname to the allowed hosts.
|
||||
genai_secrets:
|
||||
title: Connector secrets properties for an OpenAI connector
|
||||
description: Defines secrets for connectors when type is `.gen-ai`.
|
||||
description: |
|
||||
Defines secrets for connectors when type is `.gen-ai`. Supports both API key authentication (OpenAI, Azure OpenAI, and `Other`) and PKI authentication (`Other` provider only). PKI fields must be base64-encoded PEM content.
|
||||
type: object
|
||||
properties:
|
||||
apiKey:
|
||||
type: string
|
||||
description: The OpenAI API key.
|
||||
description: |
|
||||
The API key for authentication. For OpenAI and Azure OpenAI providers, it is required. For the `Other` provider, it is required if you do not use PKI authentication. With PKI, you can also optionally include an API key if the OpenAI-compatible service supports or requires one.
|
||||
certificateData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM certificate content for PKI authentication (Other provider only). Required for PKI.
|
||||
minLength: 1
|
||||
privateKeyData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM private key content for PKI authentication (Other provider only). Required for PKI.
|
||||
minLength: 1
|
||||
caData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM CA certificate content for PKI authentication (Other provider only). Optional.
|
||||
minLength: 1
|
||||
required: []
|
||||
opsgenie_secrets:
|
||||
title: Connector secrets properties for an Opsgenie connector
|
||||
required:
|
||||
|
@ -65629,6 +65648,52 @@ components:
|
|||
description: |
|
||||
A password for HTTP basic authentication. It is applicable only when `usesBasic` is `true`.
|
||||
type: string
|
||||
genai_openai_other_config:
|
||||
title: Connector request properties for an OpenAI connector with Other provider
|
||||
description: |
|
||||
Defines properties for connectors when type is `.gen-ai` and the API provider is `Other` (OpenAI-compatible service), including optional PKI authentication.
|
||||
type: object
|
||||
required:
|
||||
- apiProvider
|
||||
- apiUrl
|
||||
- defaultModel
|
||||
properties:
|
||||
apiProvider:
|
||||
type: string
|
||||
description: The OpenAI API provider.
|
||||
enum:
|
||||
- Other
|
||||
apiUrl:
|
||||
type: string
|
||||
description: The OpenAI-compatible API endpoint.
|
||||
defaultModel:
|
||||
type: string
|
||||
description: The default model to use for requests.
|
||||
certificateData:
|
||||
type: string
|
||||
description: PEM-encoded certificate content.
|
||||
minLength: 1
|
||||
privateKeyData:
|
||||
type: string
|
||||
description: PEM-encoded private key content.
|
||||
minLength: 1
|
||||
caData:
|
||||
type: string
|
||||
description: PEM-encoded CA certificate content.
|
||||
minLength: 1
|
||||
verificationMode:
|
||||
type: string
|
||||
description: SSL verification mode for PKI authentication.
|
||||
enum:
|
||||
- full
|
||||
- certificate
|
||||
- none
|
||||
default: full
|
||||
headers:
|
||||
type: object
|
||||
description: Custom headers to include in requests.
|
||||
additionalProperties:
|
||||
type: string
|
||||
defender_secrets:
|
||||
title: Connector secrets properties for a Microsoft Defender for Endpoint connector
|
||||
required:
|
||||
|
|
|
@ -723,6 +723,7 @@ paths:
|
|||
- $ref: '#/components/schemas/jira_config'
|
||||
- $ref: '#/components/schemas/genai_azure_config'
|
||||
- $ref: '#/components/schemas/genai_openai_config'
|
||||
- $ref: '#/components/schemas/genai_openai_other_config'
|
||||
- $ref: '#/components/schemas/opsgenie_config'
|
||||
- $ref: '#/components/schemas/pagerduty_config'
|
||||
- $ref: '#/components/schemas/sentinelone_config'
|
||||
|
@ -48521,12 +48522,30 @@ components:
|
|||
The URL of the incoming webhook. If you are using the `xpack.actions.allowedHosts` setting, add the hostname to the allowed hosts.
|
||||
genai_secrets:
|
||||
title: Connector secrets properties for an OpenAI connector
|
||||
description: Defines secrets for connectors when type is `.gen-ai`.
|
||||
description: |
|
||||
Defines secrets for connectors when type is `.gen-ai`. Supports both API key authentication (OpenAI, Azure OpenAI, and `Other`) and PKI authentication (`Other` provider only). PKI fields must be base64-encoded PEM content.
|
||||
type: object
|
||||
properties:
|
||||
apiKey:
|
||||
type: string
|
||||
description: The OpenAI API key.
|
||||
description: |
|
||||
The API key for authentication. For OpenAI and Azure OpenAI providers, it is required. For the `Other` provider, it is required if you do not use PKI authentication. With PKI, you can also optionally include an API key if the OpenAI-compatible service supports or requires one.
|
||||
certificateData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM certificate content for PKI authentication (Other provider only). Required for PKI.
|
||||
minLength: 1
|
||||
privateKeyData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM private key content for PKI authentication (Other provider only). Required for PKI.
|
||||
minLength: 1
|
||||
caData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM CA certificate content for PKI authentication (Other provider only). Optional.
|
||||
minLength: 1
|
||||
required: []
|
||||
opsgenie_secrets:
|
||||
title: Connector secrets properties for an Opsgenie connector
|
||||
required:
|
||||
|
@ -48696,6 +48715,52 @@ components:
|
|||
description: |
|
||||
A password for HTTP basic authentication. It is applicable only when `usesBasic` is `true`.
|
||||
type: string
|
||||
genai_openai_other_config:
|
||||
title: Connector request properties for an OpenAI connector with Other provider
|
||||
description: |
|
||||
Defines properties for connectors when type is `.gen-ai` and the API provider is `Other` (OpenAI-compatible service), including optional PKI authentication.
|
||||
type: object
|
||||
required:
|
||||
- apiProvider
|
||||
- apiUrl
|
||||
- defaultModel
|
||||
properties:
|
||||
apiProvider:
|
||||
type: string
|
||||
description: The OpenAI API provider.
|
||||
enum:
|
||||
- Other
|
||||
apiUrl:
|
||||
type: string
|
||||
description: The OpenAI-compatible API endpoint.
|
||||
defaultModel:
|
||||
type: string
|
||||
description: The default model to use for requests.
|
||||
certificateData:
|
||||
type: string
|
||||
description: PEM-encoded certificate content.
|
||||
minLength: 1
|
||||
privateKeyData:
|
||||
type: string
|
||||
description: PEM-encoded private key content.
|
||||
minLength: 1
|
||||
caData:
|
||||
type: string
|
||||
description: PEM-encoded CA certificate content.
|
||||
minLength: 1
|
||||
verificationMode:
|
||||
type: string
|
||||
description: SSL verification mode for PKI authentication.
|
||||
enum:
|
||||
- full
|
||||
- certificate
|
||||
- none
|
||||
default: full
|
||||
headers:
|
||||
type: object
|
||||
description: Custom headers to include in requests.
|
||||
additionalProperties:
|
||||
type: string
|
||||
defender_secrets:
|
||||
title: Connector secrets properties for a Microsoft Defender for Endpoint connector
|
||||
required:
|
||||
|
|
|
@ -164,6 +164,8 @@ actions:
|
|||
- $ref: '../../x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/genai_azure_config.yaml'
|
||||
# OpenAI (.gen-ai)
|
||||
- $ref: '../../x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/genai_openai_config.yaml'
|
||||
# Other OpenAI (.gen-ai)
|
||||
- $ref: '../../x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/genai_openai_other_config.yaml'
|
||||
# Opsgenie (.opsgenie)
|
||||
- $ref: '../../x-pack/platform/plugins/shared/actions/docs/openapi/components/schemas/opsgenie_config.yaml'
|
||||
# PagerDuty (.pagerduty)
|
||||
|
|
|
@ -807,7 +807,7 @@
|
|||
"@kbn/safer-lodash-set": "link:src/platform/packages/shared/kbn-safer-lodash-set",
|
||||
"@kbn/saml-provider-plugin": "link:x-pack/test/security_api_integration/plugins/saml_provider",
|
||||
"@kbn/sample-task-plugin": "link:x-pack/platform/test/plugin_api_integration/plugins/sample_task_plugin",
|
||||
"@kbn/sample-task-plugin-update-by-query": "link:x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget",
|
||||
"@kbn/sample-task-plugin-update-by-query": "link:x-pack/platform/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget",
|
||||
"@kbn/saved-object-export-transforms-plugin": "link:src/platform/test/plugin_functional/plugins/saved_object_export_transforms",
|
||||
"@kbn/saved-object-import-warnings-plugin": "link:src/platform/test/plugin_functional/plugins/saved_object_import_warnings",
|
||||
"@kbn/saved-object-test-plugin": "link:x-pack/platform/test/saved_object_api_integration/common/plugins/saved_object_test_plugin",
|
||||
|
@ -970,7 +970,7 @@
|
|||
"@kbn/synthetics-plugin": "link:x-pack/solutions/observability/plugins/synthetics",
|
||||
"@kbn/task-manager-dependencies-plugin": "link:x-pack/platform/plugins/private/task_manager_dependencies",
|
||||
"@kbn/task-manager-fixture-plugin": "link:x-pack/platform/test/alerting_api_integration/common/plugins/task_manager_fixture",
|
||||
"@kbn/task-manager-performance-plugin": "link:x-pack/test/plugin_api_perf/plugins/task_manager_performance",
|
||||
"@kbn/task-manager-performance-plugin": "link:x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance",
|
||||
"@kbn/task-manager-plugin": "link:x-pack/platform/plugins/shared/task_manager",
|
||||
"@kbn/telemetry": "link:src/platform/packages/shared/kbn-telemetry",
|
||||
"@kbn/telemetry-collection-manager-plugin": "link:src/platform/plugins/shared/telemetry_collection_manager",
|
||||
|
@ -1167,7 +1167,7 @@
|
|||
"deep-freeze-strict": "^1.1.1",
|
||||
"deepmerge": "^4.3.1",
|
||||
"del": "^6.1.0",
|
||||
"diff": "^8.0.1",
|
||||
"diff": "^8.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"elastic-apm-node": "^4.13.0",
|
||||
"email-addresses": "^5.0.0",
|
||||
|
@ -1301,6 +1301,8 @@
|
|||
"redux-thunk": "^2.4.2",
|
||||
"redux-thunks": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rehype-raw": "5.1.0",
|
||||
"rehype-sanitize": "4.0.0",
|
||||
"remark-gfm": "1.0.0",
|
||||
"remark-parse-no-trim": "^8.0.4",
|
||||
"remark-stringify": "^8.0.3",
|
||||
|
|
|
@ -41,7 +41,7 @@ run(
|
|||
})
|
||||
: [{ name: path.parse(journeyPath).name, path: journeyPath }];
|
||||
|
||||
const skippedFilePath = 'x-pack/test/scalability/disabled_scalability_tests.json';
|
||||
const skippedFilePath = 'x-pack/platform/test/scalability/disabled_scalability_tests.json';
|
||||
const skipped: string[] = JSON.parse(
|
||||
fs.readFileSync(path.resolve(REPO_ROOT, skippedFilePath), 'utf8')
|
||||
).map((relativePath: string) => path.resolve(REPO_ROOT, relativePath));
|
||||
|
@ -89,7 +89,7 @@ run(
|
|||
cmd: 'node',
|
||||
args: [
|
||||
'scripts/functional_tests',
|
||||
['--config', 'x-pack/test/scalability/config.ts'],
|
||||
['--config', 'x-pack/platform/test/scalability/config.ts'],
|
||||
kibanaBuildDir ? ['--kibana-install-dir', kibanaBuildDir] : [],
|
||||
'--debug',
|
||||
'--logToFile',
|
||||
|
|
|
@ -132,7 +132,7 @@ export const reportingCsvExportProvider = ({
|
|||
name: panelTitle,
|
||||
exportType: reportType,
|
||||
label: 'CSV',
|
||||
icon: 'documents',
|
||||
icon: 'tableDensityNormal',
|
||||
generateAssetExport: generateReportingJobCSV,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
|
|
|
@ -37,6 +37,9 @@ import {
|
|||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
ALERT_TIME_RANGE,
|
||||
ALERT_UPDATED_AT,
|
||||
ALERT_UPDATED_BY_USER_ID,
|
||||
ALERT_UPDATED_BY_USER_NAME,
|
||||
ALERT_URL,
|
||||
ALERT_UUID,
|
||||
ALERT_WORKFLOW_ASSIGNEE_IDS,
|
||||
|
@ -213,6 +216,21 @@ export const alertFieldMap = {
|
|||
array: false,
|
||||
required: false,
|
||||
},
|
||||
[ALERT_UPDATED_AT]: {
|
||||
type: 'date',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
[ALERT_UPDATED_BY_USER_ID]: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
[ALERT_UPDATED_BY_USER_NAME]: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
[ALERT_URL]: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
|
|
|
@ -107,6 +107,9 @@ const AlertOptional = rt.partial({
|
|||
'kibana.alert.severity_improving': schemaBoolean,
|
||||
'kibana.alert.start': schemaDate,
|
||||
'kibana.alert.time_range': schemaDateRange,
|
||||
'kibana.alert.updated_at': schemaDate,
|
||||
'kibana.alert.updated_by.user.id': schemaString,
|
||||
'kibana.alert.updated_by.user.name': schemaString,
|
||||
'kibana.alert.url': schemaString,
|
||||
'kibana.alert.workflow_assignee_ids': schemaStringArray,
|
||||
'kibana.alert.workflow_status': schemaString,
|
||||
|
|
|
@ -207,6 +207,9 @@ const SecurityAlertOptional = rt.partial({
|
|||
})
|
||||
),
|
||||
'kibana.alert.time_range': schemaDateRange,
|
||||
'kibana.alert.updated_at': schemaDate,
|
||||
'kibana.alert.updated_by.user.id': schemaString,
|
||||
'kibana.alert.updated_by.user.name': schemaString,
|
||||
'kibana.alert.url': schemaString,
|
||||
'kibana.alert.user.criticality_level': schemaString,
|
||||
'kibana.alert.workflow_assignee_ids': schemaStringArray,
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
// this file was generated, and should not be edited by hand
|
||||
// ---------------------------------- WARNING ----------------------------------
|
||||
import * as rt from 'io-ts';
|
||||
import type { Either } from 'fp-ts/Either';
|
||||
import type { Either } from 'fp-ts/lib/Either';
|
||||
import { AlertSchema } from './alert_schema';
|
||||
const ISO_DATE_PATTERN = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/;
|
||||
export const IsoDateString = new rt.Type<string, string, unknown>(
|
||||
|
@ -107,6 +107,9 @@ const StreamsAlertOptional = rt.partial({
|
|||
'kibana.alert.severity_improving': schemaBoolean,
|
||||
'kibana.alert.start': schemaDate,
|
||||
'kibana.alert.time_range': schemaDateRange,
|
||||
'kibana.alert.updated_at': schemaDate,
|
||||
'kibana.alert.updated_by.user.id': schemaString,
|
||||
'kibana.alert.updated_by.user.name': schemaString,
|
||||
'kibana.alert.url': schemaString,
|
||||
'kibana.alert.workflow_assignee_ids': schemaStringArray,
|
||||
'kibana.alert.workflow_status': schemaString,
|
||||
|
|
|
@ -382,7 +382,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
|
|||
batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`,
|
||||
indexBlocks: `${ELASTICSEARCH_DOCS}index-modules-blocks.html#index-block-settings`,
|
||||
remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`,
|
||||
unfreezeApi: `${ELASTICSEARCH_DOCS}/unfreeze-index-api.html`,
|
||||
unfreezeApi: `${ELASTICSEARCH_DOCS}unfreeze-index-api.html`,
|
||||
reindexWithPipeline: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-with-an-ingest-pipeline`,
|
||||
logsDatastream: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/logs-data-stream.html`,
|
||||
usingLogsDbIndexModeWithESSecurity: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/detections-logsdb-index-mode-impact.html`,
|
||||
|
|
|
@ -129,6 +129,8 @@ export const OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN =
|
|||
'observability:aiAssistantSearchConnectorIndexPattern';
|
||||
export const OBSERVABILITY_SEARCH_EXCLUDED_DATA_TIERS = 'observability:searchExcludedDataTiers';
|
||||
export const OBSERVABILITY_ENABLE_STREAMS_UI = 'observability:enableStreamsUI';
|
||||
export const OBSERVABILITY_STREAMS_ENABLE_SIGNIFICANT_EVENTS =
|
||||
'observability:streamsEnableSignificantEvents';
|
||||
|
||||
// Reporting settings
|
||||
export const XPACK_REPORTING_CUSTOM_PDF_LOGO_ID = 'xpackReporting:customPdfLogo';
|
||||
|
|
|
@ -71,6 +71,15 @@ const ALERT_REASON = `${ALERT_NAMESPACE}.reason` as const;
|
|||
// kibana.alert.start - timestamp when the alert is first active
|
||||
const ALERT_START = `${ALERT_NAMESPACE}.start` as const;
|
||||
|
||||
// kibana.alert.updated_at - timestamp when the alert was last updated
|
||||
const ALERT_UPDATED_AT = `${ALERT_NAMESPACE}.updated_at` as const;
|
||||
|
||||
// kibana.alert.updated_by.user.id - user id of the user that last updated the alert
|
||||
const ALERT_UPDATED_BY_USER_ID = `${ALERT_NAMESPACE}.updated_by.user.id` as const;
|
||||
|
||||
// kibana.alert.updated_by.user.name - user name of the user that last updated the alert
|
||||
const ALERT_UPDATED_BY_USER_NAME = `${ALERT_NAMESPACE}.updated_by.user.name` as const;
|
||||
|
||||
// kibana.alert.status - active/recovered status of alert
|
||||
const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const;
|
||||
|
||||
|
@ -163,6 +172,9 @@ export const fields = {
|
|||
ALERT_RULE_UUID,
|
||||
ALERT_SEVERITY_IMPROVING,
|
||||
ALERT_START,
|
||||
ALERT_UPDATED_AT,
|
||||
ALERT_UPDATED_BY_USER_ID,
|
||||
ALERT_UPDATED_BY_USER_NAME,
|
||||
ALERT_STATUS,
|
||||
ALERT_TIME_RANGE,
|
||||
ALERT_URL,
|
||||
|
@ -210,6 +222,9 @@ export {
|
|||
ALERT_RULE_UUID,
|
||||
ALERT_SEVERITY_IMPROVING,
|
||||
ALERT_START,
|
||||
ALERT_UPDATED_AT,
|
||||
ALERT_UPDATED_BY_USER_ID,
|
||||
ALERT_UPDATED_BY_USER_NAME,
|
||||
ALERT_STATUS,
|
||||
ALERT_TIME_RANGE,
|
||||
ALERT_URL,
|
||||
|
|
|
@ -24,6 +24,7 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [
|
|||
settings.OBSERVABILITY_APM_ENABLE_SERVICE_INVENTORY_TABLE_SEARCH_BAR,
|
||||
settings.OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID,
|
||||
settings.OBSERVABILITY_ENABLE_STREAMS_UI,
|
||||
settings.OBSERVABILITY_STREAMS_ENABLE_SIGNIFICANT_EVENTS,
|
||||
];
|
||||
|
||||
export const OBSERVABILITY_AI_ASSISTANT_PROJECT_SETTINGS = [
|
||||
|
|
|
@ -42,6 +42,7 @@ describe('TabbedModal', () => {
|
|||
|
||||
return (
|
||||
<EuiFieldText
|
||||
data-test-subj="log-user-input-field"
|
||||
placeholder="Placeholder text"
|
||||
value={state.inputText}
|
||||
onChange={onChange}
|
||||
|
@ -57,7 +58,7 @@ describe('TabbedModal', () => {
|
|||
},
|
||||
};
|
||||
|
||||
it('renders the modal component', async () => {
|
||||
it("when a single tab definition is passed it simply renders it's content into the modal component without tabs", async () => {
|
||||
render(
|
||||
<TabbedModal
|
||||
tabs={[tabDefinition]}
|
||||
|
@ -66,6 +67,24 @@ describe('TabbedModal', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(tabDefinition.name)).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('log-user-input-field')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(await screen.findByTestId(tabDefinition.modalActionBtn!.dataTestSubj));
|
||||
|
||||
expect(mockedHandlerFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the tabbed modal with tabs for tab definition with length greater than 1', async () => {
|
||||
render(
|
||||
<TabbedModal
|
||||
tabs={[tabDefinition, { ...tabDefinition, id: 'anotherTab', name: 'another tab' }]}
|
||||
defaultSelectedTabId="logUserInput"
|
||||
onClose={modalOnCloseHandler}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(tabDefinition.name)).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(await screen.findByTestId(tabDefinition.modalActionBtn!.dataTestSubj));
|
||||
|
|
|
@ -127,21 +127,29 @@ const TabbedModalInner: FC<ITabbedModalInner> = ({
|
|||
);
|
||||
|
||||
const renderTabs = useCallback(() => {
|
||||
return tabs.map((tab, index) => {
|
||||
return (
|
||||
<EuiTab
|
||||
key={index}
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
disabled={tab.disabled}
|
||||
prepend={tab.prepend}
|
||||
append={tab.append}
|
||||
data-test-subj={tab.id}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
);
|
||||
});
|
||||
if (tabs.length === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiTabs>
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<EuiTab
|
||||
key={index}
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
disabled={tab.disabled}
|
||||
prepend={tab.prepend}
|
||||
append={tab.append}
|
||||
data-test-subj={tab.id}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
);
|
||||
})}
|
||||
</EuiTabs>
|
||||
);
|
||||
}, [onSelectedTabChanged, selectedTabId, tabs]);
|
||||
|
||||
const modalPositionOverrideStyles: React.CSSProperties = {
|
||||
|
@ -170,17 +178,22 @@ const TabbedModalInner: FC<ITabbedModalInner> = ({
|
|||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<Fragment>
|
||||
<EuiTabs>{renderTabs()}</EuiTabs>
|
||||
<Fragment>{renderTabs()}</Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
{React.createElement(function RenderSelectedTabContent() {
|
||||
useLayoutEffect(onTabContentRender, []);
|
||||
return (
|
||||
<SelectedTabContent
|
||||
{...{
|
||||
state: selectedTabState,
|
||||
dispatch,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
css={{ display: 'contents' }}
|
||||
data-test-subj={`tabbedModal-${selectedTabId}-content`}
|
||||
>
|
||||
<SelectedTabContent
|
||||
{...{
|
||||
state: selectedTabState,
|
||||
dispatch,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
|
|
|
@ -659,10 +659,22 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
description: 'Enable the new logs overview component.',
|
||||
},
|
||||
},
|
||||
'cases:incrementalIdDisplay:enabled': {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: 'Display the incremental id of a case in the relevant pages',
|
||||
},
|
||||
},
|
||||
'observability:enableStreamsUI': {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: 'Enable Streams UI.',
|
||||
},
|
||||
},
|
||||
'observability:streamsEnableSignificantEvents': {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: 'Enable significant events in streams.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -171,5 +171,7 @@ export interface UsageStats {
|
|||
'securitySolution:excludedDataTiersForRuleExecution': string[];
|
||||
'securitySolution:maxUnassociatedNotes': number;
|
||||
'observability:searchExcludedDataTiers': string[];
|
||||
'cases:incrementalIdDisplay:enabled': boolean;
|
||||
'observability:enableStreamsUI': boolean;
|
||||
'observability:streamsEnableSignificantEvents': boolean;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@ import { render, screen } from '@testing-library/react';
|
|||
import { userEvent } from '@testing-library/user-event';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
import { ExportMenu } from './export_popover';
|
||||
import { ExportMenu } from './export_integrations';
|
||||
import type { IShareContext } from '../context';
|
||||
import type { ExportShareConfig } from '../../types';
|
||||
|
||||
const mockShareContext: IShareContext = {
|
||||
shareMenuItems: [
|
||||
|
@ -51,7 +52,11 @@ const mockShareContext: IShareContext = {
|
|||
onClose: jest.fn(),
|
||||
};
|
||||
|
||||
function ExportPopoverRender() {
|
||||
function ExportPopoverRender({
|
||||
shareContext = mockShareContext,
|
||||
}: {
|
||||
shareContext?: IShareContext;
|
||||
}) {
|
||||
const [clickTarget, setClickTarget] = React.useState<HTMLElement | null>();
|
||||
|
||||
return (
|
||||
|
@ -59,7 +64,7 @@ function ExportPopoverRender() {
|
|||
{Boolean(clickTarget) && (
|
||||
<ExportMenu
|
||||
shareContext={{
|
||||
...mockShareContext,
|
||||
...shareContext,
|
||||
anchorElement: clickTarget!,
|
||||
}}
|
||||
/>
|
||||
|
@ -69,7 +74,7 @@ function ExportPopoverRender() {
|
|||
);
|
||||
}
|
||||
|
||||
describe('ExportPopover', () => {
|
||||
describe('Export Integrations', () => {
|
||||
it('renders a popover with the list of registered export types', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
@ -83,4 +88,33 @@ describe('ExportPopover', () => {
|
|||
expect(screen.getByText(label)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('will invoke the export integrations generateAssetExport config method if it is the singular export type available', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const singleExportShareContext: IShareContext = {
|
||||
...mockShareContext,
|
||||
shareMenuItems: [
|
||||
{
|
||||
shareType: 'integration',
|
||||
groupId: 'export',
|
||||
id: 'csv',
|
||||
config: {
|
||||
icon: 'empty',
|
||||
label: 'CSV',
|
||||
generateAssetExport: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
} as unknown as ExportShareConfig,
|
||||
],
|
||||
};
|
||||
|
||||
render(<ExportPopoverRender shareContext={singleExportShareContext} />);
|
||||
|
||||
await user.click(screen.getByText('click me'));
|
||||
|
||||
expect(
|
||||
(singleExportShareContext.shareMenuItems[0] as ExportShareConfig).config.generateAssetExport
|
||||
).toHaveBeenCalled();
|
||||
expect(singleExportShareContext.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -190,6 +190,7 @@ function ManagedFlyout({
|
|||
<EuiCodeBlock
|
||||
data-test-subj="exportAssetValue"
|
||||
css={{ overflowWrap: 'break-word' }}
|
||||
overflowHeight={360}
|
||||
language={exportIntegration.config.copyAssetURIConfig.contentType}
|
||||
isCopyable
|
||||
copyAriaLabel={i18n.translate('share.export.copyPostURLAriaLabel', {
|
||||
|
@ -286,34 +287,65 @@ function ExportMenuPopover({ intl }: ExportMenuProps) {
|
|||
setIsFlyoutVisible(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// when there is only one share menu item, and no export derivatives registered,
|
||||
// we want to open the flyout and not the popover
|
||||
if (
|
||||
exportIntegrations.length === 1 &&
|
||||
exportDerivatives.length === 0 &&
|
||||
!selectedMenuItemMeta
|
||||
) {
|
||||
openFlyout(exportIntegrations[0]);
|
||||
}
|
||||
}, [exportIntegrations, exportDerivatives, openFlyout, selectedMenuItemMeta]);
|
||||
|
||||
const flyoutOnCloseHandler = useCallback(() => {
|
||||
return exportIntegrations.length === 1 && exportDerivatives.length === 0
|
||||
? onClose()
|
||||
: setIsFlyoutVisible(false);
|
||||
}, [exportDerivatives.length, exportIntegrations.length, onClose]);
|
||||
const exportIntegrationInteractionHandler = useCallback(
|
||||
async (menuItem: ExportShareConfig) => {
|
||||
if (
|
||||
!menuItem.config.copyAssetURIConfig &&
|
||||
!menuItem.config.generateAssetComponent &&
|
||||
menuItem.config.generateAssetExport
|
||||
) {
|
||||
await menuItem.config
|
||||
.generateAssetExport({
|
||||
intl,
|
||||
optimizedForPrinting: false,
|
||||
})
|
||||
.finally(() => {
|
||||
onClose();
|
||||
});
|
||||
} else {
|
||||
openFlyout(menuItem);
|
||||
}
|
||||
},
|
||||
[intl, onClose, openFlyout]
|
||||
);
|
||||
|
||||
const flyoutRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const canSkipDisplayingPopover = useMemo<boolean>(() => {
|
||||
// when there is only one export share menu item, and no export derivatives registered,
|
||||
// we'd like to skip displaying the popover
|
||||
return exportIntegrations.length === 1 && !exportDerivatives.length;
|
||||
}, [exportIntegrations, exportDerivatives]);
|
||||
|
||||
const flyoutOnCloseHandler = useCallback(() => {
|
||||
setIsFlyoutVisible(false);
|
||||
if (canSkipDisplayingPopover) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose, canSkipDisplayingPopover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canSkipDisplayingPopover && !selectedMenuItemMeta) {
|
||||
exportIntegrationInteractionHandler(exportIntegrations[0]);
|
||||
}
|
||||
}, [
|
||||
exportIntegrationInteractionHandler,
|
||||
exportIntegrations,
|
||||
onClose,
|
||||
selectedMenuItemMeta,
|
||||
canSkipDisplayingPopover,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiWrappingPopover
|
||||
isOpen={!isFlyoutVisible}
|
||||
data-test-subj="exportPopover"
|
||||
isOpen={!isFlyoutVisible && !canSkipDisplayingPopover}
|
||||
button={anchorElement!}
|
||||
closePopover={onClose}
|
||||
panelPaddingSize="s"
|
||||
panelProps={{
|
||||
'data-test-subj': 'exportPopoverPanel',
|
||||
}}
|
||||
>
|
||||
<EuiListGroup flush>
|
||||
{exportIntegrations.map((menuItem) => (
|
||||
|
@ -328,7 +360,7 @@ function ExportMenuPopover({ intl }: ExportMenuProps) {
|
|||
label={menuItem.config.label}
|
||||
data-test-subj={`exportMenuItem-${menuItem.config.label}`}
|
||||
isDisabled={menuItem.config.disabled}
|
||||
onClick={() => openFlyout(menuItem)}
|
||||
onClick={exportIntegrationInteractionHandler.bind(null, menuItem)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
))}
|
||||
|
@ -368,6 +400,7 @@ function ExportMenuPopover({ intl }: ExportMenuProps) {
|
|||
? selectedMenuItem.config.flyoutSizing || {}
|
||||
: {})}
|
||||
>
|
||||
{/* TODO: remove this global style once https://github.com/elastic/eui/issues/8801 is resolved */}
|
||||
<Global
|
||||
// @ts-expect-error -- we pass a z-index specifying important so we override the default z-index, so solve a known bug,
|
||||
// where when `headerZindexLocation` is set to `above`, the popover panel z-index is not high enough
|
|
@ -7,4 +7,4 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { ExportMenu } from './export_popover';
|
||||
export { ExportMenu } from './export_integrations';
|
|
@ -34,7 +34,7 @@ export const ShareMenuTabs = () => {
|
|||
tabs.push(linkTab);
|
||||
}
|
||||
|
||||
// Embed is disabled in the serverless offering, hence the need to check that we received it
|
||||
// Embed is disabled in the serverless offering, hence the need to check if the embed tab should be shown
|
||||
if (
|
||||
shareMenuItems.some(({ shareType }) => shareType === 'embed') &&
|
||||
!objectTypeMeta?.config?.embed?.disabled
|
||||
|
|
|
@ -16,7 +16,7 @@ import { ShowShareMenuOptions } from '../types';
|
|||
import { ShareRegistry } from './share_menu_registry';
|
||||
import type { ShareConfigs } from '../types';
|
||||
import { ShareMenu } from '../components/share_tabs';
|
||||
import { ExportMenu } from '../components/export_popover';
|
||||
import { ExportMenu } from '../components/export_integrations';
|
||||
|
||||
interface ShareMenuManagerStartDeps {
|
||||
core: CoreStart;
|
||||
|
|
|
@ -11273,11 +11273,23 @@
|
|||
"description": "Enable the new logs overview component."
|
||||
}
|
||||
},
|
||||
"cases:incrementalIdDisplay:enabled": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Display the incremental id of a case in the relevant pages"
|
||||
}
|
||||
},
|
||||
"observability:enableStreamsUI": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Enable Streams UI."
|
||||
}
|
||||
},
|
||||
"observability:streamsEnableSignificantEvents": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Enable significant events in streams."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ export class ExportPageObject extends FtrService {
|
|||
}
|
||||
|
||||
async isExportPopoverOpen() {
|
||||
return await this.testSubjects.exists('exportPopover');
|
||||
return await this.testSubjects.exists('exportPopoverPanel');
|
||||
}
|
||||
|
||||
async isPopoverItemEnabled(label: string) {
|
||||
|
|
|
@ -228,6 +228,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cases.files.maxSize (number?)',
|
||||
'xpack.cases.markdownPlugins.lens (boolean?)',
|
||||
'xpack.cases.stack.enabled (boolean?)',
|
||||
'xpack.cases.incrementalId.enabled (boolean?)',
|
||||
'xpack.ccr.ui.enabled (boolean?)',
|
||||
'xpack.cloud.base_url (string?)',
|
||||
'xpack.cloud.cname (string?)',
|
||||
|
|
|
@ -1586,8 +1586,8 @@
|
|||
"@kbn/sample-log-parser/*": ["x-pack/platform/packages/shared/kbn-sample-parser/*"],
|
||||
"@kbn/sample-task-plugin": ["x-pack/platform/test/plugin_api_integration/plugins/sample_task_plugin"],
|
||||
"@kbn/sample-task-plugin/*": ["x-pack/platform/test/plugin_api_integration/plugins/sample_task_plugin/*"],
|
||||
"@kbn/sample-task-plugin-update-by-query": ["x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget"],
|
||||
"@kbn/sample-task-plugin-update-by-query/*": ["x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/*"],
|
||||
"@kbn/sample-task-plugin-update-by-query": ["x-pack/platform/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget"],
|
||||
"@kbn/sample-task-plugin-update-by-query/*": ["x-pack/platform/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/*"],
|
||||
"@kbn/saved-object-export-transforms-plugin": ["src/platform/test/plugin_functional/plugins/saved_object_export_transforms"],
|
||||
"@kbn/saved-object-export-transforms-plugin/*": ["src/platform/test/plugin_functional/plugins/saved_object_export_transforms/*"],
|
||||
"@kbn/saved-object-import-warnings-plugin": ["src/platform/test/plugin_functional/plugins/saved_object_import_warnings"],
|
||||
|
@ -1972,8 +1972,8 @@
|
|||
"@kbn/task-manager-dependencies-plugin/*": ["x-pack/platform/plugins/private/task_manager_dependencies/*"],
|
||||
"@kbn/task-manager-fixture-plugin": ["x-pack/platform/test/alerting_api_integration/common/plugins/task_manager_fixture"],
|
||||
"@kbn/task-manager-fixture-plugin/*": ["x-pack/platform/test/alerting_api_integration/common/plugins/task_manager_fixture/*"],
|
||||
"@kbn/task-manager-performance-plugin": ["x-pack/test/plugin_api_perf/plugins/task_manager_performance"],
|
||||
"@kbn/task-manager-performance-plugin/*": ["x-pack/test/plugin_api_perf/plugins/task_manager_performance/*"],
|
||||
"@kbn/task-manager-performance-plugin": ["x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance"],
|
||||
"@kbn/task-manager-performance-plugin/*": ["x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance/*"],
|
||||
"@kbn/task-manager-plugin": ["x-pack/platform/plugins/shared/task_manager"],
|
||||
"@kbn/task-manager-plugin/*": ["x-pack/platform/plugins/shared/task_manager/*"],
|
||||
"@kbn/telemetry": ["src/platform/packages/shared/kbn-telemetry"],
|
||||
|
|
|
@ -31,6 +31,7 @@ export {
|
|||
type FunctionCallingMode,
|
||||
type ToolChoice,
|
||||
type ChatCompleteAPI,
|
||||
type ChatCompleteAPIResponse,
|
||||
type ChatCompleteOptions,
|
||||
type ChatCompleteCompositeResponse,
|
||||
type ChatCompletionTokenCountEvent,
|
||||
|
@ -43,7 +44,6 @@ export {
|
|||
type ChatCompleteRetryConfiguration,
|
||||
type ChatCompletionTokenCount,
|
||||
type BoundChatCompleteAPI,
|
||||
type BoundChatCompleteOptions,
|
||||
type UnboundChatCompleteOptions,
|
||||
withoutTokenCountEvents,
|
||||
withoutChunkEvents,
|
||||
|
@ -75,7 +75,6 @@ export {
|
|||
type Output,
|
||||
type OutputEvent,
|
||||
type BoundOutputAPI,
|
||||
type BoundOutputOptions,
|
||||
type UnboundOutputOptions,
|
||||
isOutputCompleteEvent,
|
||||
isOutputUpdateEvent,
|
||||
|
@ -139,7 +138,6 @@ export { type Model, ModelFamily, ModelPlatform, ModelProvider } from './src/mod
|
|||
|
||||
export {
|
||||
type BoundPromptAPI,
|
||||
type BoundPromptOptions,
|
||||
type Prompt,
|
||||
type PromptAPI,
|
||||
type PromptCompositeResponse,
|
||||
|
@ -152,3 +150,5 @@ export {
|
|||
type UnboundPromptOptions,
|
||||
createPrompt,
|
||||
} from './src/prompt';
|
||||
|
||||
export { type BoundOptions, type UnboundOptions, bindApi } from './src/bind';
|
||||
|
|
|
@ -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 { FunctionCallingMode } from '../chat_complete';
|
||||
|
||||
export interface BoundOptions {
|
||||
functionCalling?: FunctionCallingMode;
|
||||
connectorId: string;
|
||||
}
|
||||
|
||||
type BoundOptionKey = keyof BoundOptions;
|
||||
|
||||
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
|
||||
|
||||
export type UnboundOptions<TOptions extends BoundOptions> = DistributiveOmit<
|
||||
TOptions,
|
||||
BoundOptionKey
|
||||
>;
|
||||
|
||||
type BindableAPI = (options: any, ...rest: any[]) => any;
|
||||
|
||||
type BoundAPI<F extends BindableAPI> = F extends (options: infer O, ...rest: infer R) => infer Ret
|
||||
? O extends BoundOptions
|
||||
? (options: UnboundOptions<O>, ...rest: R) => Ret
|
||||
: never
|
||||
: never;
|
||||
|
||||
export function bindApi<T extends BindableAPI, U extends BoundOptions>(
|
||||
api: T,
|
||||
boundParams: U
|
||||
): BoundAPI<T>;
|
||||
|
||||
export function bindApi(api: BindableAPI, boundParams: BoundOptions) {
|
||||
const { functionCalling, connectorId } = boundParams;
|
||||
return (params: UnboundOptions<BoundOptions>) => {
|
||||
return api({
|
||||
...params,
|
||||
functionCalling,
|
||||
connectorId,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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 { type BoundOptions, type UnboundOptions, bindApi } from './bind_api';
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Overwrite } from 'utility-types';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { ToolCallsOf, ToolOptions } from './tools';
|
||||
import type { ToolCallsOf, ToolChoiceType, ToolOptions } from './tools';
|
||||
import type { Message } from './messages';
|
||||
import type { ChatCompletionEvent, ChatCompletionTokenCount } from './events';
|
||||
import type { ChatCompleteMetadata } from './metadata';
|
||||
|
@ -55,33 +56,36 @@ import type { ChatCompleteMetadata } from './metadata';
|
|||
* });
|
||||
* ```
|
||||
*/
|
||||
export type ChatCompleteAPI = <
|
||||
TToolOptions extends ToolOptions = ToolOptions,
|
||||
TStream extends boolean = false
|
||||
>(
|
||||
options: ChatCompleteOptions<TToolOptions, TStream>
|
||||
) => ChatCompleteCompositeResponse<TToolOptions, TStream>;
|
||||
|
||||
export interface DefaultChatCompleteOptions {
|
||||
stream: false;
|
||||
tools: {};
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}
|
||||
|
||||
type ChatCompleteCompositeResponseOptions = Pick<
|
||||
ChatCompleteOptions,
|
||||
'stream' | 'tools' | 'toolChoice'
|
||||
>;
|
||||
|
||||
type ChatCompleteResponseOptions = Pick<ChatCompleteOptions, 'tools' | 'toolChoice'>;
|
||||
|
||||
export type ChatCompleteAPIResponse<TOptions extends ChatCompleteOptions = ChatCompleteOptions> =
|
||||
ChatCompleteCompositeResponse<Overwrite<DefaultChatCompleteOptions, TOptions>>;
|
||||
|
||||
export type ChatCompleteAPI = <TOptions extends ChatCompleteOptions>(
|
||||
options: TOptions
|
||||
) => ChatCompleteAPIResponse<TOptions>;
|
||||
|
||||
/**
|
||||
* Options used to call the {@link ChatCompleteAPI}
|
||||
*/
|
||||
export type ChatCompleteOptions<
|
||||
TToolOptions extends ToolOptions = ToolOptions,
|
||||
TStream extends boolean = false
|
||||
> = {
|
||||
export type ChatCompleteOptions = {
|
||||
/**
|
||||
* The ID of the connector to use.
|
||||
* Must be an inference connector, or an error will be thrown.
|
||||
*/
|
||||
connectorId: string;
|
||||
/**
|
||||
* Set to true to enable streaming, which will change the API response type from
|
||||
* a single {@link ChatCompleteResponse} promise
|
||||
* to a {@link ChatCompleteStreamResponse} event observable.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
stream?: TStream;
|
||||
/**
|
||||
* Optional system message for the LLM.
|
||||
*/
|
||||
|
@ -126,7 +130,15 @@ export type ChatCompleteOptions<
|
|||
* Note that defaults are very fine, so only use this if you really have a reason to do so.
|
||||
*/
|
||||
retryConfiguration?: ChatCompleteRetryConfiguration;
|
||||
} & TToolOptions;
|
||||
/**
|
||||
* Set to true to enable streaming, which will change the API response type from
|
||||
* a single {@link ChatCompleteResponse} promise
|
||||
* to a {@link ChatCompleteStreamResponse} event observable.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
stream?: boolean;
|
||||
} & ToolOptions;
|
||||
|
||||
export interface ChatCompleteRetryConfiguration {
|
||||
/**
|
||||
|
@ -160,25 +172,26 @@ export interface ChatCompleteRetryConfiguration {
|
|||
* whether API was called with stream mode enabled or not.
|
||||
*/
|
||||
export type ChatCompleteCompositeResponse<
|
||||
TToolOptions extends ToolOptions = ToolOptions,
|
||||
TStream extends boolean = false
|
||||
> = TStream extends true
|
||||
? ChatCompleteStreamResponse<TToolOptions>
|
||||
: Promise<ChatCompleteResponse<TToolOptions>>;
|
||||
TOptions extends ChatCompleteCompositeResponseOptions = ChatCompleteCompositeResponseOptions
|
||||
> =
|
||||
| (true extends TOptions['stream'] ? ChatCompleteStreamResponse<TOptions> : never)
|
||||
| (false extends TOptions['stream'] ? Promise<ChatCompleteResponse<TOptions>> : never);
|
||||
|
||||
/**
|
||||
* Response from the {@link ChatCompleteAPI} when streaming is enabled.
|
||||
*
|
||||
* Observable of {@link ChatCompletionEvent}
|
||||
*/
|
||||
export type ChatCompleteStreamResponse<TToolOptions extends ToolOptions = ToolOptions> = Observable<
|
||||
ChatCompletionEvent<TToolOptions>
|
||||
>;
|
||||
export type ChatCompleteStreamResponse<
|
||||
TOptions extends ChatCompleteResponseOptions = ChatCompleteResponseOptions
|
||||
> = Observable<ChatCompletionEvent<TOptions>>;
|
||||
|
||||
/**
|
||||
* Response from the {@link ChatCompleteAPI} when streaming is not enabled.
|
||||
*/
|
||||
export interface ChatCompleteResponse<TToolOptions extends ToolOptions = ToolOptions> {
|
||||
export interface ChatCompleteResponse<
|
||||
TOptions extends ChatCompleteResponseOptions = ChatCompleteResponseOptions
|
||||
> {
|
||||
/**
|
||||
* The text content of the LLM response.
|
||||
*/
|
||||
|
@ -186,7 +199,7 @@ export interface ChatCompleteResponse<TToolOptions extends ToolOptions = ToolOpt
|
|||
/**
|
||||
* The eventual tool calls performed by the LLM.
|
||||
*/
|
||||
toolCalls: ToolCallsOf<TToolOptions>['toolCalls'];
|
||||
toolCalls: ToolCallsOf<TOptions>['toolCalls'];
|
||||
/**
|
||||
* Token counts
|
||||
*/
|
||||
|
|
|
@ -5,31 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ChatCompleteOptions, ChatCompleteCompositeResponse } from './api';
|
||||
import type { ToolOptions } from './tools';
|
||||
|
||||
/**
|
||||
* Static options used to call the {@link BoundChatCompleteAPI}
|
||||
*/
|
||||
export type BoundChatCompleteOptions<
|
||||
TToolOptions extends ToolOptions = ToolOptions,
|
||||
TStream extends boolean = false
|
||||
> = Pick<ChatCompleteOptions<TToolOptions, TStream>, 'connectorId' | 'functionCalling'>;
|
||||
import { BoundOptions, UnboundOptions } from '../bind/bind_api';
|
||||
import type { ChatCompleteOptions, ChatCompleteAPIResponse } from './api';
|
||||
|
||||
/**
|
||||
* Options used to call the {@link BoundChatCompleteAPI}
|
||||
*/
|
||||
export type UnboundChatCompleteOptions<
|
||||
TToolOptions extends ToolOptions = ToolOptions,
|
||||
TStream extends boolean = false
|
||||
> = Omit<ChatCompleteOptions<TToolOptions, TStream>, 'connectorId' | 'functionCalling'>;
|
||||
export type UnboundChatCompleteOptions = UnboundOptions<ChatCompleteOptions>;
|
||||
|
||||
/**
|
||||
* Version of {@link ChatCompleteAPI} that got pre-bound to a set of static parameters
|
||||
*/
|
||||
export type BoundChatCompleteAPI = <
|
||||
TToolOptions extends ToolOptions = ToolOptions,
|
||||
TStream extends boolean = false
|
||||
>(
|
||||
options: UnboundChatCompleteOptions<TToolOptions, TStream>
|
||||
) => ChatCompleteCompositeResponse<TToolOptions, TStream>;
|
||||
export type BoundChatCompleteAPI = <TChatCompleteOptions extends UnboundChatCompleteOptions>(
|
||||
options: TChatCompleteOptions
|
||||
) => ChatCompleteAPIResponse<TChatCompleteOptions & BoundOptions>;
|
||||
|
|
|
@ -8,17 +8,14 @@
|
|||
export type {
|
||||
ChatCompleteCompositeResponse,
|
||||
ChatCompleteAPI,
|
||||
ChatCompleteAPIResponse,
|
||||
ChatCompleteOptions,
|
||||
FunctionCallingMode,
|
||||
ChatCompleteStreamResponse,
|
||||
ChatCompleteResponse,
|
||||
ChatCompleteRetryConfiguration,
|
||||
} from './api';
|
||||
export type {
|
||||
BoundChatCompleteAPI,
|
||||
BoundChatCompleteOptions,
|
||||
UnboundChatCompleteOptions,
|
||||
} from './bound_api';
|
||||
export type { BoundChatCompleteAPI, UnboundChatCompleteOptions } from './bound_api';
|
||||
export {
|
||||
ChatCompletionEventType,
|
||||
type ChatCompletionMessageEvent,
|
||||
|
|
|
@ -39,12 +39,14 @@ export type ToolCallbacksOf<TToolOptions extends ToolOptions> = TToolOptions ext
|
|||
*/
|
||||
export type ToolResponsesOf<TTools extends Record<string, ToolDefinition> | undefined> =
|
||||
TTools extends Record<string, ToolDefinition>
|
||||
? Array<
|
||||
ValuesType<{
|
||||
[TName in keyof TTools & string]: ToolCall<TName, ToolResponseOf<TTools[TName]>>;
|
||||
}>
|
||||
>
|
||||
: never[];
|
||||
? keyof TTools extends never
|
||||
? []
|
||||
: Array<
|
||||
ValuesType<{
|
||||
[TName in keyof TTools & string]: ToolCall<TName, ToolResponseOf<TTools[TName]>>;
|
||||
}>
|
||||
>
|
||||
: [];
|
||||
|
||||
/**
|
||||
* Utility type to infer the tool call response shape.
|
||||
|
@ -113,7 +115,7 @@ export type ToolCallsOf<TToolOptions extends ToolOptions> = TToolOptions extends
|
|||
: {
|
||||
toolCalls: ToolResponsesOf<ToolsOfChoice<TToolOptions>>;
|
||||
}
|
||||
: { toolCalls: never };
|
||||
: { toolCalls: [] };
|
||||
|
||||
/**
|
||||
* Represents a tool call from the LLM before correctly converted to the schema type.
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 { expectAssignable, expectType } from 'tsd';
|
||||
import type {
|
||||
ChatCompleteAPI,
|
||||
ChatCompleteCompositeResponse,
|
||||
ChatCompleteResponse,
|
||||
ChatCompleteStreamResponse,
|
||||
} from '../api';
|
||||
import { Message, MessageRole } from '../messages';
|
||||
import { ToolChoiceType, ToolDefinition, ToolResponseOf } from '../tools';
|
||||
|
||||
declare const mockApi: ChatCompleteAPI;
|
||||
const minimalMessages: Message[] = [{ role: MessageRole.User, content: 'hello' }];
|
||||
|
||||
const getWeatherToolDefinition = {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { location: { type: 'string' } },
|
||||
required: ['location'],
|
||||
},
|
||||
description: 'Get weather',
|
||||
} satisfies ToolDefinition;
|
||||
|
||||
const getStockPriceToolDefinition = {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { location: { type: 'string' } },
|
||||
required: ['location'],
|
||||
},
|
||||
description: 'Get weather',
|
||||
} satisfies ToolDefinition;
|
||||
|
||||
const myTools = {
|
||||
get_weather: getWeatherToolDefinition,
|
||||
get_stock_price: getStockPriceToolDefinition,
|
||||
};
|
||||
|
||||
type MyTools = typeof myTools;
|
||||
|
||||
/**
|
||||
* No specified settings
|
||||
*/
|
||||
declare const defaultToolResponse: ChatCompleteResponse<{}>;
|
||||
|
||||
expectType<{ content: string; toolCalls: [] }>(defaultToolResponse);
|
||||
|
||||
/**
|
||||
* No tools
|
||||
*/
|
||||
declare const emptyToolResponse: ChatCompleteResponse<{
|
||||
tools: {};
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}>;
|
||||
|
||||
expectType<{ content: string; toolCalls: [] }>(emptyToolResponse);
|
||||
|
||||
/**
|
||||
* Defined tools
|
||||
*/
|
||||
declare const specificToolResponse: ChatCompleteResponse<{
|
||||
tools: MyTools;
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}>;
|
||||
|
||||
expectType<{
|
||||
content: string;
|
||||
toolCalls: Array<{
|
||||
toolCallId: string;
|
||||
function: {
|
||||
name: keyof MyTools;
|
||||
arguments: ToolResponseOf<MyTools['get_stock_price'] | MyTools['get_weather']>;
|
||||
};
|
||||
}>;
|
||||
}>(specificToolResponse);
|
||||
|
||||
if (specificToolResponse.toolCalls && specificToolResponse.toolCalls.length > 0) {
|
||||
const firstToolCall = specificToolResponse.toolCalls[0];
|
||||
expectAssignable<'get_weather' | 'get_stock_price'>(firstToolCall.function.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* stream: false = no streaming
|
||||
*/
|
||||
type CompositeNonStream = ChatCompleteCompositeResponse<{
|
||||
stream: false;
|
||||
tools: MyTools;
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}>;
|
||||
|
||||
expectType<Promise<ChatCompleteResponse<{ tools: MyTools; toolChoice: ToolChoiceType.auto }>>>(
|
||||
{} as CompositeNonStream
|
||||
);
|
||||
/**
|
||||
* stream: false = streaming
|
||||
*/
|
||||
|
||||
type CompositeStream = ChatCompleteCompositeResponse<{
|
||||
stream: true;
|
||||
tools: MyTools;
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* stream: boolean = union
|
||||
*/
|
||||
expectType<ChatCompleteStreamResponse<{ tools: MyTools; toolChoice: ToolChoiceType.auto }>>(
|
||||
{} as CompositeStream
|
||||
);
|
||||
|
||||
type CompositeBooleanStream = ChatCompleteCompositeResponse<{
|
||||
stream: boolean;
|
||||
tools: MyTools;
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}>;
|
||||
|
||||
expectAssignable<
|
||||
| Promise<ChatCompleteResponse<{ tools: MyTools; toolChoice: ToolChoiceType.auto }>>
|
||||
| ChatCompleteStreamResponse<{ tools: MyTools; toolChoice: ToolChoiceType.auto }>
|
||||
>({} as CompositeBooleanStream);
|
||||
|
||||
/**
|
||||
* tools inference for runtime code
|
||||
*/
|
||||
const resWithTools = mockApi({
|
||||
connectorId: 'c1',
|
||||
messages: minimalMessages,
|
||||
tools: { get_weather: getWeatherToolDefinition },
|
||||
});
|
||||
|
||||
// defaults to non-streaming
|
||||
expectType<
|
||||
Promise<
|
||||
ChatCompleteResponse<{
|
||||
tools: { get_weather: typeof getWeatherToolDefinition };
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}>
|
||||
>
|
||||
>(resWithTools);
|
||||
resWithTools.then((r) => {
|
||||
if (r.toolCalls) {
|
||||
expectType<'get_weather'>(r.toolCalls[0].function.name);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* tools inference for runtime code + stream: true
|
||||
*/
|
||||
const resStreamWithTools = mockApi({
|
||||
connectorId: 'c1',
|
||||
messages: minimalMessages,
|
||||
stream: true,
|
||||
tools: { get_weather: getWeatherToolDefinition },
|
||||
});
|
||||
expectType<
|
||||
ChatCompleteStreamResponse<{
|
||||
tools: { get_weather: typeof getWeatherToolDefinition };
|
||||
toolChoice: ToolChoiceType.auto;
|
||||
}>
|
||||
>(resStreamWithTools);
|
||||
|
||||
/**
|
||||
* toolChoice = none should be toolCalls:[]
|
||||
*/
|
||||
|
||||
const resToolChoiceNone = mockApi({
|
||||
connectorId: 'c1',
|
||||
messages: minimalMessages,
|
||||
tools: { get_weather: getWeatherToolDefinition },
|
||||
toolChoice: ToolChoiceType.none,
|
||||
});
|
||||
resToolChoiceNone.then((r) => {
|
||||
expectType<[]>(r.toolCalls);
|
||||
});
|
||||
|
||||
/**
|
||||
* specific tool choice = only specified tool is typed
|
||||
*/
|
||||
const resToolChoiceSpecific = mockApi({
|
||||
connectorId: 'c1',
|
||||
messages: minimalMessages,
|
||||
tools: myTools,
|
||||
toolChoice: { type: 'function', function: 'get_weather' as const },
|
||||
});
|
||||
|
||||
expectType<
|
||||
Promise<
|
||||
ChatCompleteResponse<{
|
||||
tools: MyTools;
|
||||
toolChoice: { type: 'function'; function: 'get_weather' };
|
||||
}>
|
||||
>
|
||||
>(resToolChoiceSpecific);
|
||||
|
||||
resToolChoiceSpecific.then((r) => {
|
||||
if (r.toolCalls) {
|
||||
expectAssignable<
|
||||
Array<{
|
||||
toolCallId: string;
|
||||
function: {
|
||||
name: 'get_weather';
|
||||
arguments: ToolResponseOf<typeof getWeatherToolDefinition>;
|
||||
};
|
||||
}>
|
||||
>(r.toolCalls);
|
||||
}
|
||||
});
|
|
@ -7,15 +7,7 @@
|
|||
|
||||
import type { OutputOptions, OutputCompositeResponse } from './api';
|
||||
import type { ToolSchema } from '../chat_complete/tool_schema';
|
||||
|
||||
/**
|
||||
* Static options used to call the {@link BoundOutputAPI}
|
||||
*/
|
||||
export type BoundOutputOptions<
|
||||
TId extends string = string,
|
||||
TOutputSchema extends ToolSchema | undefined = ToolSchema | undefined,
|
||||
TStream extends boolean = false
|
||||
> = Pick<OutputOptions<TId, TOutputSchema, TStream>, 'connectorId' | 'functionCalling'>;
|
||||
import { UnboundOptions } from '../bind/bind_api';
|
||||
|
||||
/**
|
||||
* Options used to call the {@link BoundOutputAPI}
|
||||
|
@ -24,7 +16,7 @@ export type UnboundOutputOptions<
|
|||
TId extends string = string,
|
||||
TOutputSchema extends ToolSchema | undefined = ToolSchema | undefined,
|
||||
TStream extends boolean = false
|
||||
> = Omit<OutputOptions<TId, TOutputSchema, TStream>, 'connectorId' | 'functionCalling'>;
|
||||
> = UnboundOptions<OutputOptions<TId, TOutputSchema, TStream>>;
|
||||
|
||||
/**
|
||||
* Version of {@link OutputAPI} that got pre-bound to a set of static parameters
|
||||
|
|
|
@ -12,7 +12,7 @@ export type {
|
|||
OutputResponse,
|
||||
OutputStreamResponse,
|
||||
} from './api';
|
||||
export type { BoundOutputAPI, BoundOutputOptions, UnboundOutputOptions } from './bound_api';
|
||||
export type { BoundOutputAPI, UnboundOutputOptions } from './bound_api';
|
||||
export {
|
||||
OutputEventType,
|
||||
type OutputCompleteEvent,
|
||||
|
|
|
@ -6,33 +6,30 @@
|
|||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
import { Optional } from 'utility-types';
|
||||
import { Overwrite, Assign } from 'utility-types';
|
||||
import {
|
||||
ChatCompleteOptions,
|
||||
ChatCompleteResponse,
|
||||
ChatCompleteStreamResponse,
|
||||
Message,
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
ToolOptions,
|
||||
} from '../chat_complete';
|
||||
import { Prompt, ToolOptionsOfPrompt } from './types';
|
||||
import {
|
||||
ChatCompleteAPIResponse,
|
||||
ChatCompleteCompositeResponse,
|
||||
ChatCompleteResponse,
|
||||
ChatCompleteStreamResponse,
|
||||
} from '../chat_complete/api';
|
||||
|
||||
/**
|
||||
* Generate a response with the LLM based on a structured Prompt.
|
||||
*/
|
||||
export type PromptAPI = <
|
||||
TPrompt extends Prompt = Prompt,
|
||||
TOtherOptions extends PromptOptions<TPrompt> = PromptOptions<TPrompt>
|
||||
>(
|
||||
options: TOtherOptions & { prompt: TPrompt }
|
||||
) => PromptCompositeResponse<{ prompt: TPrompt } & TOtherOptions>;
|
||||
type PromptChatCompleteOptions = Omit<ChatCompleteOptions, 'messages' | 'system'>;
|
||||
|
||||
/**
|
||||
* Options for the {@link PromptAPI}
|
||||
*/
|
||||
export interface PromptOptions<TPrompt extends Prompt = Prompt>
|
||||
extends Optional<Omit<ChatCompleteOptions, 'messages' | 'system' | 'stream'>, 'temperature'> {
|
||||
export interface PromptOptions<TPrompt extends Prompt = Prompt> extends PromptChatCompleteOptions {
|
||||
prompt: TPrompt;
|
||||
input: z.input<TPrompt['input']>;
|
||||
stream?: boolean;
|
||||
prevMessages?: Message[];
|
||||
}
|
||||
|
||||
|
@ -42,20 +39,52 @@ export interface PromptOptions<TPrompt extends Prompt = Prompt>
|
|||
* whether API was called with stream mode enabled or not.
|
||||
*/
|
||||
export type PromptCompositeResponse<TPromptOptions extends PromptOptions = PromptOptions> =
|
||||
TPromptOptions['stream'] extends true
|
||||
? PromptStreamResponse<TPromptOptions['prompt']>
|
||||
: Promise<PromptResponse<TPromptOptions['prompt']>>;
|
||||
ChatCompleteCompositeResponse<
|
||||
Omit<TPromptOptions, 'tools' | 'toolChoice'> &
|
||||
MergeToolOptions<ToolOptionsOfPrompt<TPromptOptions['prompt']>, TPromptOptions>
|
||||
>;
|
||||
|
||||
type MergeToolOptions<TLeft extends ToolOptions, TRight extends ToolOptions> = Overwrite<
|
||||
Pick<TLeft, 'tools' | 'toolChoice'>,
|
||||
{
|
||||
toolChoice: TRight['toolChoice'] extends ToolChoice
|
||||
? TRight['toolChoice']
|
||||
: TLeft['toolChoice'];
|
||||
tools: TLeft['tools'] extends Record<string, ToolDefinition>
|
||||
? TRight['tools'] extends Record<string, ToolDefinition>
|
||||
? Assign<TLeft['tools'], TRight['tools']>
|
||||
: TLeft['tools']
|
||||
: TRight['tools'] extends Record<string, ToolDefinition>
|
||||
? TRight['tools']
|
||||
: {};
|
||||
}
|
||||
>;
|
||||
|
||||
type ChatCompleteOptionsOfPromptOptions<TPromptOptions extends PromptOptions = PromptOptions> = {
|
||||
messages: Message[];
|
||||
} & Omit<TPromptOptions, 'tools' | 'toolChoice'> &
|
||||
MergeToolOptions<ToolOptionsOfPrompt<TPromptOptions['prompt']>, TPromptOptions>;
|
||||
|
||||
export type PromptResponseOf<
|
||||
TPrompt extends Prompt,
|
||||
TStream extends boolean = false
|
||||
> = PromptCompositeResponse<Overwrite<PromptOptions<TPrompt>, { stream: TStream }>>;
|
||||
|
||||
export type PromptAPIResponse<TPromptOptions extends PromptOptions = PromptOptions> =
|
||||
ChatCompleteAPIResponse<ChatCompleteOptionsOfPromptOptions<TPromptOptions>>;
|
||||
|
||||
/**
|
||||
* Response from the {@link PromptAPI} when streaming is not enabled.
|
||||
*/
|
||||
export type PromptResponse<TPrompt extends Prompt = Prompt> = ChatCompleteResponse<
|
||||
ToolOptionsOfPrompt<TPrompt>
|
||||
>;
|
||||
export type PromptResponse<TPromptOptions extends PromptOptions = PromptOptions> =
|
||||
ChatCompleteResponse<ChatCompleteOptionsOfPromptOptions<TPromptOptions>>;
|
||||
|
||||
/**
|
||||
* Response from the {@link PromptAPI} in streaming mode.
|
||||
*/
|
||||
export type PromptStreamResponse<TPrompt extends Prompt = Prompt> = ChatCompleteStreamResponse<
|
||||
ToolOptionsOfPrompt<TPrompt>
|
||||
>;
|
||||
export type PromptStreamResponse<TPromptOptions extends PromptOptions = PromptOptions> =
|
||||
ChatCompleteStreamResponse<ChatCompleteOptionsOfPromptOptions<TPromptOptions>>;
|
||||
|
||||
export type PromptAPI = <TPrompt extends Prompt, TPromptOptions extends PromptOptions>(
|
||||
options: PromptOptions<TPrompt> & TPromptOptions
|
||||
) => PromptAPIResponse<TPromptOptions>;
|
||||
|
|
|
@ -5,31 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PromptOptions, PromptCompositeResponse } from './api';
|
||||
import { BoundOptions, UnboundOptions } from '../bind/bind_api';
|
||||
import type { PromptOptions, PromptAPIResponse } from './api';
|
||||
import { Prompt } from './types';
|
||||
|
||||
/**
|
||||
* Static options used to call the {@link BoundPromptAPI}
|
||||
*/
|
||||
export type BoundPromptOptions<TPromptOptions extends PromptOptions = PromptOptions> = Pick<
|
||||
PromptOptions<TPromptOptions['prompt']>,
|
||||
'connectorId' | 'functionCalling'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Options used to call the {@link BoundPromptAPI}
|
||||
*/
|
||||
export type UnboundPromptOptions<TPromptOptions extends PromptOptions = PromptOptions> = Omit<
|
||||
PromptOptions<TPromptOptions['prompt']>,
|
||||
'connectorId' | 'functionCalling'
|
||||
>;
|
||||
export type UnboundPromptOptions<TPromptOptions extends PromptOptions = PromptOptions> =
|
||||
UnboundOptions<TPromptOptions>;
|
||||
|
||||
/**
|
||||
* Version of {@link PromptAPI} that got pre-bound to a set of static parameters
|
||||
*/
|
||||
export type BoundPromptAPI = <
|
||||
TPrompt extends Prompt = Prompt,
|
||||
TPromptOptions extends PromptOptions<TPrompt> = PromptOptions<TPrompt>
|
||||
>(
|
||||
options: UnboundPromptOptions<TPromptOptions & { prompt: TPrompt }>
|
||||
) => PromptCompositeResponse<TPromptOptions & { prompt: TPrompt }>;
|
||||
export type BoundPromptAPI = <TPrompt extends Prompt, TPromptOptions extends UnboundPromptOptions>(
|
||||
options: { prompt: TPrompt } & TPromptOptions
|
||||
) => PromptAPIResponse<BoundOptions & TPromptOptions>;
|
||||
|
|
|
@ -12,5 +12,5 @@ export type {
|
|||
PromptResponse,
|
||||
PromptStreamResponse,
|
||||
} from './api';
|
||||
export type { BoundPromptAPI, BoundPromptOptions, UnboundPromptOptions } from './bound_api';
|
||||
export type { BoundPromptAPI, UnboundPromptOptions } from './bound_api';
|
||||
export type { Prompt, PromptFactory, PromptVersion, ToolOptionsOfPrompt } from './types';
|
||||
|
|
|
@ -0,0 +1,323 @@
|
|||
/*
|
||||
* 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 { expectAssignable, expectType } from 'tsd';
|
||||
import { z } from '@kbn/zod';
|
||||
import type {
|
||||
PromptAPI,
|
||||
PromptCompositeResponse,
|
||||
PromptOptions,
|
||||
PromptResponse,
|
||||
PromptResponseOf,
|
||||
PromptStreamResponse,
|
||||
} from '../api';
|
||||
import type { Prompt, PromptVersion, MustachePromptTemplate } from '../types';
|
||||
import {
|
||||
ToolCallsOf,
|
||||
ToolChoiceType,
|
||||
ToolDefinition,
|
||||
ToolResponseOf,
|
||||
} from '../../chat_complete/tools';
|
||||
|
||||
declare const mockApi: PromptAPI;
|
||||
|
||||
const MinimalPromptInputSchema = z.object({
|
||||
query: z.string(),
|
||||
});
|
||||
type MinimalPromptInput = z.input<typeof MinimalPromptInputSchema>;
|
||||
const minimalInput: MinimalPromptInput = { query: 'test' };
|
||||
|
||||
const mustacheTemplate: MustachePromptTemplate = {
|
||||
mustache: {
|
||||
template: 'User query: {{query}}',
|
||||
},
|
||||
};
|
||||
|
||||
const getWeatherToolDefinition = {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { location: { type: 'string' } },
|
||||
required: ['location'],
|
||||
},
|
||||
description: 'Get weather',
|
||||
} satisfies ToolDefinition;
|
||||
|
||||
const getStockPriceToolDefinition = {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { ticker: { type: 'string' } },
|
||||
required: ['ticker'],
|
||||
},
|
||||
description: 'Get stock price',
|
||||
} satisfies ToolDefinition;
|
||||
|
||||
const promptTools = {
|
||||
get_weather: getWeatherToolDefinition,
|
||||
get_stock_price: getStockPriceToolDefinition,
|
||||
};
|
||||
|
||||
type PromptTools = typeof promptTools;
|
||||
|
||||
/**
|
||||
* Prompts for testing
|
||||
*/
|
||||
const promptNoTools = {
|
||||
name: 'NoToolsPrompt',
|
||||
description: 'A prompt with no tools defined in its version.',
|
||||
input: MinimalPromptInputSchema,
|
||||
versions: [
|
||||
{
|
||||
template: mustacheTemplate,
|
||||
// No tools or toolChoice here
|
||||
} satisfies PromptVersion,
|
||||
],
|
||||
} satisfies Prompt<MinimalPromptInput>;
|
||||
|
||||
type PromptNoTools = typeof promptNoTools;
|
||||
|
||||
const promptWithToolsAuto = {
|
||||
name: 'WithToolsAutoPrompt',
|
||||
description: 'A prompt with tools and toolChoice:auto.',
|
||||
input: MinimalPromptInputSchema,
|
||||
versions: [
|
||||
{
|
||||
template: mustacheTemplate,
|
||||
tools: promptTools,
|
||||
toolChoice: ToolChoiceType.auto,
|
||||
} satisfies PromptVersion<{ tools: PromptTools; toolChoice: ToolChoiceType.auto }>,
|
||||
],
|
||||
} satisfies Prompt<MinimalPromptInput>;
|
||||
|
||||
type PromptWithToolsAuto = typeof promptWithToolsAuto;
|
||||
|
||||
const promptWithToolsNone = {
|
||||
name: 'WithToolsNonePrompt',
|
||||
description: 'A prompt with tools and toolChoice:none.',
|
||||
input: MinimalPromptInputSchema,
|
||||
versions: [
|
||||
{
|
||||
template: mustacheTemplate,
|
||||
tools: promptTools,
|
||||
toolChoice: ToolChoiceType.none,
|
||||
} satisfies PromptVersion<{ tools: PromptTools; toolChoice: ToolChoiceType.none }>,
|
||||
],
|
||||
} satisfies Prompt<MinimalPromptInput>;
|
||||
|
||||
const promptWithGetWeatherToolChoice = {
|
||||
name: 'WithToolsSpecificPrompt',
|
||||
description: 'A prompt with tools and specific toolChoice.',
|
||||
input: MinimalPromptInputSchema,
|
||||
versions: [
|
||||
{
|
||||
template: mustacheTemplate,
|
||||
tools: promptTools,
|
||||
toolChoice: { function: 'get_weather' as const },
|
||||
} satisfies PromptVersion<{
|
||||
tools: PromptTools;
|
||||
toolChoice: { function: 'get_weather' };
|
||||
}>,
|
||||
],
|
||||
} satisfies Prompt<MinimalPromptInput>;
|
||||
|
||||
/**
|
||||
* Test PromptResponse type
|
||||
*/
|
||||
|
||||
// No tools in prompt version
|
||||
declare const noToolsPromptResponse: Awaited<PromptResponseOf<PromptNoTools>>;
|
||||
|
||||
expectType<{ content: string; toolCalls: [] }>(noToolsPromptResponse);
|
||||
|
||||
// With tools defined in prompt version
|
||||
declare const specificToolPromptResponse: Awaited<PromptResponseOf<PromptWithToolsAuto>>;
|
||||
expectType<{
|
||||
content: string;
|
||||
toolCalls: Array<{
|
||||
toolCallId: string;
|
||||
function: {
|
||||
name: keyof PromptTools;
|
||||
arguments: ToolResponseOf<PromptTools[keyof PromptTools]>;
|
||||
};
|
||||
}>;
|
||||
}>(specificToolPromptResponse);
|
||||
|
||||
if (specificToolPromptResponse.toolCalls && specificToolPromptResponse.toolCalls.length > 0) {
|
||||
const firstToolCall = specificToolPromptResponse.toolCalls[0];
|
||||
expectAssignable<'get_weather' | 'get_stock_price'>(firstToolCall.function.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PromptCompositeResponse type
|
||||
*/
|
||||
type PromptOptsWithToolsAuto = PromptOptions<PromptWithToolsAuto>;
|
||||
|
||||
// stream: false
|
||||
type CompositeNonStream = PromptCompositeResponse<PromptOptsWithToolsAuto & { stream: false }>;
|
||||
|
||||
expectType<PromptResponseOf<PromptWithToolsAuto>>({} as CompositeNonStream);
|
||||
|
||||
// stream: true
|
||||
type PromptOptsWithToolsAutoStreamResponse = PromptStreamResponse<PromptOptsWithToolsAuto>;
|
||||
|
||||
expectType<PromptResponseOf<PromptWithToolsAuto, true>>(
|
||||
{} as PromptOptsWithToolsAutoStreamResponse
|
||||
);
|
||||
|
||||
// stream: boolean
|
||||
type CompositeResponse = PromptCompositeResponse<PromptOptsWithToolsAuto & { stream: boolean }>;
|
||||
|
||||
expectAssignable<
|
||||
Promise<PromptResponse<PromptOptsWithToolsAuto>> | PromptStreamResponse<PromptOptsWithToolsAuto>
|
||||
>({} as CompositeResponse);
|
||||
|
||||
/**
|
||||
* Test PromptAPI runtime inference
|
||||
*/
|
||||
|
||||
// Default stream (false), with tools
|
||||
const resWithTools = mockApi({
|
||||
connectorId: 'c1',
|
||||
prompt: promptWithToolsAuto,
|
||||
input: minimalInput,
|
||||
});
|
||||
|
||||
expectType<Promise<PromptResponse<PromptOptsWithToolsAuto>>>(resWithTools);
|
||||
resWithTools.then((r) => {
|
||||
if (r.toolCalls && r.toolCalls.length > 0) {
|
||||
expectType<'get_weather' | 'get_stock_price'>(r.toolCalls[0].function.name);
|
||||
expectAssignable<ToolResponseOf<PromptTools[keyof PromptTools]>>(
|
||||
r.toolCalls[0].function.arguments
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// stream: true, with tools
|
||||
const resStreamWithTools = mockApi({
|
||||
connectorId: 'c1',
|
||||
prompt: promptWithToolsAuto,
|
||||
input: minimalInput,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
expectType<PromptStreamResponse<PromptOptsWithToolsAuto>>(resStreamWithTools);
|
||||
|
||||
// toolChoice: none
|
||||
const resToolChoiceNone = mockApi({
|
||||
connectorId: 'c1',
|
||||
prompt: promptWithToolsNone,
|
||||
input: minimalInput,
|
||||
});
|
||||
resToolChoiceNone.then((r) => {
|
||||
expectType<[]>(r.toolCalls);
|
||||
});
|
||||
|
||||
// toolChoice: specific function
|
||||
const resToolChoiceSpecific = mockApi({
|
||||
connectorId: 'c1',
|
||||
prompt: promptWithGetWeatherToolChoice,
|
||||
input: minimalInput,
|
||||
});
|
||||
|
||||
expectType<PromptResponseOf<typeof promptWithGetWeatherToolChoice>>(resToolChoiceSpecific);
|
||||
|
||||
resToolChoiceSpecific.then((r) => {
|
||||
if (r.toolCalls && r.toolCalls.length > 0) {
|
||||
// With a specific tool choice, only that tool should be possible.
|
||||
// The ToolOptionsOfPrompt<typeof promptWithToolsSpecific> should correctly narrow this.
|
||||
expectType<'get_weather'>(r.toolCalls[0].function.name);
|
||||
expectType<ToolResponseOf<PromptTools['get_weather']>>(r.toolCalls[0].function.arguments);
|
||||
}
|
||||
});
|
||||
|
||||
// New tool definition for API call options
|
||||
const getGeolocationToolDefinition = {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { address: { type: 'string' } },
|
||||
required: ['address'],
|
||||
},
|
||||
description: 'Get geolocation for an address',
|
||||
} satisfies ToolDefinition;
|
||||
|
||||
const apiCallTools = {
|
||||
get_geolocation: getGeolocationToolDefinition,
|
||||
};
|
||||
|
||||
// scenario: no tools in prompt, tools in API call
|
||||
const resNoPromptToolsWithApiTools = mockApi({
|
||||
connectorId: 'c1',
|
||||
prompt: promptNoTools,
|
||||
input: minimalInput,
|
||||
tools: apiCallTools, // Tools provided directly in API call options
|
||||
toolChoice: ToolChoiceType.auto,
|
||||
});
|
||||
|
||||
// should just return the tools from the API call
|
||||
expectType<
|
||||
Promise<{
|
||||
content: string;
|
||||
toolCalls: ToolCallsOf<{ tools: typeof apiCallTools }>['toolCalls'];
|
||||
}>
|
||||
>(resNoPromptToolsWithApiTools);
|
||||
|
||||
resNoPromptToolsWithApiTools.then((r) => {
|
||||
expectType<{
|
||||
toolCallId: string;
|
||||
function: {
|
||||
name: 'get_geolocation';
|
||||
arguments: {
|
||||
address?: string;
|
||||
};
|
||||
};
|
||||
}>(r.toolCalls[0]);
|
||||
});
|
||||
|
||||
// scenario: prompt tools + api tools
|
||||
const resPromptToolsWithApiTools = mockApi({
|
||||
connectorId: 'c1',
|
||||
prompt: promptWithToolsAuto,
|
||||
input: minimalInput,
|
||||
tools: apiCallTools,
|
||||
toolChoice: ToolChoiceType.auto,
|
||||
});
|
||||
|
||||
expectType<
|
||||
Promise<PromptResponse<PromptOptions<PromptWithToolsAuto> & { tools: typeof apiCallTools }>>
|
||||
>(resPromptToolsWithApiTools);
|
||||
|
||||
// merges tools from prompt with api
|
||||
resPromptToolsWithApiTools.then((r) => {
|
||||
if (r.toolCalls && r.toolCalls.length > 0) {
|
||||
expectType<'get_geolocation' | 'get_weather' | 'get_stock_price'>(r.toolCalls[0].function.name);
|
||||
}
|
||||
});
|
||||
|
||||
// scenario: prompt tools + api tools + api tool choice override
|
||||
const resPromptSpecificToolApiChoiceOverride = mockApi({
|
||||
connectorId: 'c1',
|
||||
prompt: promptWithGetWeatherToolChoice,
|
||||
input: minimalInput,
|
||||
tools: promptTools,
|
||||
toolChoice: { type: 'function', function: 'get_stock_price' as const },
|
||||
});
|
||||
|
||||
expectType<
|
||||
Promise<
|
||||
PromptResponse<
|
||||
PromptOptions<typeof promptWithGetWeatherToolChoice> & {
|
||||
tools: PromptTools;
|
||||
toolChoice: { function: 'get_stock_price' };
|
||||
}
|
||||
>
|
||||
>
|
||||
>(resPromptSpecificToolApiChoiceOverride);
|
||||
|
||||
resPromptSpecificToolApiChoiceOverride.then((r) => {
|
||||
if (r.toolCalls && r.toolCalls.length > 0) {
|
||||
expectType<'get_stock_price'>(r.toolCalls[0].function.name);
|
||||
}
|
||||
});
|
|
@ -64,6 +64,6 @@ export type ToolOptionsOfPrompt<TPrompt extends Prompt> = TPrompt['versions'] ex
|
|||
infer TPromptVersion
|
||||
>
|
||||
? TPromptVersion extends PromptVersion
|
||||
? Pick<TPromptVersion, 'tools'>
|
||||
? Pick<TPromptVersion, 'tools' | 'toolChoice'>
|
||||
: never
|
||||
: {};
|
||||
: never;
|
||||
|
|
|
@ -34,14 +34,13 @@ import {
|
|||
InferenceConnector,
|
||||
ChatCompleteAPI,
|
||||
ChatCompleteOptions,
|
||||
ChatCompleteCompositeResponse,
|
||||
FunctionCallingMode,
|
||||
ToolOptions,
|
||||
isChatCompletionChunkEvent,
|
||||
isChatCompletionTokenCountEvent,
|
||||
isToolValidationError,
|
||||
getConnectorDefaultModel,
|
||||
getConnectorProvider,
|
||||
ChatCompleteResponse,
|
||||
ConnectorTelemetryMetadata,
|
||||
} from '@kbn/inference-common';
|
||||
import type { ToolChoice } from './types';
|
||||
|
@ -191,15 +190,9 @@ export class InferenceChatModel extends BaseChatModel<InferenceChatModelCallOpti
|
|||
};
|
||||
}
|
||||
|
||||
async completionWithRetry(
|
||||
request: ChatCompleteOptions<ToolOptions, false>
|
||||
): Promise<ChatCompleteCompositeResponse<ToolOptions, false>>;
|
||||
async completionWithRetry(
|
||||
request: ChatCompleteOptions<ToolOptions, true>
|
||||
): Promise<ChatCompleteCompositeResponse<ToolOptions, true>>;
|
||||
async completionWithRetry(
|
||||
request: ChatCompleteOptions<ToolOptions, boolean>
|
||||
): Promise<ChatCompleteCompositeResponse<ToolOptions, boolean>> {
|
||||
completionWithRetry = <TStream extends boolean | undefined = false>(
|
||||
request: ChatCompleteOptions & { stream?: TStream }
|
||||
) => {
|
||||
return this.caller.call(async () => {
|
||||
try {
|
||||
return await this.chatComplete(request);
|
||||
|
@ -207,7 +200,7 @@ export class InferenceChatModel extends BaseChatModel<InferenceChatModelCallOpti
|
|||
throw wrapInferenceError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async _generate(
|
||||
baseMessages: BaseMessage[],
|
||||
|
@ -216,7 +209,8 @@ export class InferenceChatModel extends BaseChatModel<InferenceChatModelCallOpti
|
|||
): Promise<ChatResult> {
|
||||
const { system, messages } = messagesToInference(baseMessages);
|
||||
|
||||
let response: Awaited<ChatCompleteCompositeResponse<ToolOptions, false>>;
|
||||
let response: ChatCompleteResponse;
|
||||
|
||||
try {
|
||||
response = await this.completionWithRetry({
|
||||
...this.invocationParams(options),
|
||||
|
@ -269,7 +263,7 @@ export class InferenceChatModel extends BaseChatModel<InferenceChatModelCallOpti
|
|||
system,
|
||||
messages,
|
||||
stream: true as const,
|
||||
} as ChatCompleteOptions<ToolOptions, true>);
|
||||
});
|
||||
|
||||
const responseIterator = toAsyncIterator(response$);
|
||||
for await (const event of responseIterator) {
|
||||
|
|
|
@ -44,6 +44,26 @@ export const AttackDiscoveryAlert = z.object({
|
|||
* The (human readable) name of the connector that generated the attack discovery
|
||||
*/
|
||||
connectorName: z.string(),
|
||||
/**
|
||||
* The optional time the attack discovery alert was created
|
||||
*/
|
||||
alertStart: z.string().optional(),
|
||||
/**
|
||||
* The optional time the attack discovery alert was last updated
|
||||
*/
|
||||
alertUpdatedAt: z.string().optional(),
|
||||
/**
|
||||
* The optional id of the user who last updated the attack discovery alert
|
||||
*/
|
||||
alertUpdatedByUserId: z.string().optional(),
|
||||
/**
|
||||
* The optional username of the user who updated the attack discovery alert
|
||||
*/
|
||||
alertUpdatedByUserName: z.string().optional(),
|
||||
/**
|
||||
* The optional time the attack discovery alert workflow status was last updated
|
||||
*/
|
||||
alertWorkflowStatusUpdatedAt: z.string().optional(),
|
||||
/**
|
||||
* Details of the attack with bulleted markdown that always uses special syntax for field names and values from the source data.
|
||||
*/
|
||||
|
|
|
@ -37,6 +37,21 @@ components:
|
|||
connectorName:
|
||||
description: The (human readable) name of the connector that generated the attack discovery
|
||||
type: string
|
||||
alertStart:
|
||||
description: The optional time the attack discovery alert was created
|
||||
type: string
|
||||
alertUpdatedAt:
|
||||
description: The optional time the attack discovery alert was last updated
|
||||
type: string
|
||||
alertUpdatedByUserId:
|
||||
description: The optional id of the user who last updated the attack discovery alert
|
||||
type: string
|
||||
alertUpdatedByUserName:
|
||||
description: The optional username of the user who updated the attack discovery alert
|
||||
type: string
|
||||
alertWorkflowStatusUpdatedAt:
|
||||
description: The optional time the attack discovery alert workflow status was last updated
|
||||
type: string
|
||||
detailsMarkdown:
|
||||
description: Details of the attack with bulleted markdown that always uses special syntax for field names and values from the source data.
|
||||
type: string
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
BoundChatCompleteAPI,
|
||||
ChatCompleteResponse,
|
||||
ChatCompletionEvent,
|
||||
ToolOptions,
|
||||
UnboundChatCompleteOptions,
|
||||
} from '@kbn/inference-common';
|
||||
import { defer, from } from 'rxjs';
|
||||
|
@ -21,9 +20,7 @@ import { combineSignal } from './combine_signal';
|
|||
export function createChatComplete(options: InferenceCliClientOptions): BoundChatCompleteAPI;
|
||||
|
||||
export function createChatComplete({ connector, kibanaClient, signal }: InferenceCliClientOptions) {
|
||||
return <TToolOptions extends ToolOptions, TStream extends boolean = false>(
|
||||
options: UnboundChatCompleteOptions<TToolOptions, TStream>
|
||||
) => {
|
||||
return (options: UnboundChatCompleteOptions) => {
|
||||
const {
|
||||
messages,
|
||||
abortSignal,
|
||||
|
@ -67,16 +64,13 @@ export function createChatComplete({ connector, kibanaClient, signal }: Inferenc
|
|||
})
|
||||
.then((response) => ({ response }))
|
||||
);
|
||||
}).pipe(httpResponseIntoObservable<ChatCompletionEvent<TToolOptions>>());
|
||||
}).pipe(httpResponseIntoObservable<ChatCompletionEvent>());
|
||||
}
|
||||
|
||||
return kibanaClient.fetch<ChatCompleteResponse<TToolOptions>>(
|
||||
`/internal/inference/chat_complete`,
|
||||
{
|
||||
method: 'POST',
|
||||
body,
|
||||
signal: combineSignal(signal, abortSignal),
|
||||
}
|
||||
);
|
||||
return kibanaClient.fetch<ChatCompleteResponse>(`/internal/inference/chat_complete`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
signal: combineSignal(signal, abortSignal),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
ToolChoice,
|
||||
ToolDefinition,
|
||||
ToolMessage,
|
||||
ToolOptions,
|
||||
UserMessage,
|
||||
isChatCompletionMessageEvent,
|
||||
isChatCompletionTokenCountEvent,
|
||||
|
@ -146,15 +145,15 @@ function mapAssistantResponse({
|
|||
* @param options
|
||||
* @param cb
|
||||
*/
|
||||
export function withChatCompleteSpan<T extends ChatCompleteCompositeResponse<ToolOptions, boolean>>(
|
||||
export function withChatCompleteSpan<T extends ChatCompleteCompositeResponse>(
|
||||
options: InferenceGenerationOptions,
|
||||
cb: (span?: Span) => T
|
||||
): T;
|
||||
|
||||
export function withChatCompleteSpan(
|
||||
options: InferenceGenerationOptions,
|
||||
cb: (span?: Span) => ChatCompleteCompositeResponse<ToolOptions, boolean>
|
||||
): ChatCompleteCompositeResponse<ToolOptions, boolean> {
|
||||
cb: (span?: Span) => ChatCompleteCompositeResponse
|
||||
): ChatCompleteCompositeResponse {
|
||||
const { system, messages, model, toolChoice, tools, ...attributes } = options;
|
||||
|
||||
const next = withInferenceSpan(
|
||||
|
|
|
@ -41,6 +41,7 @@ test.describe(
|
|||
page,
|
||||
pageObjects,
|
||||
perfTracker,
|
||||
config,
|
||||
}) => {
|
||||
perfTracker.captureBundleResponses(cdp); // Start tracking
|
||||
|
||||
|
@ -75,6 +76,7 @@ test.describe(
|
|||
'lens',
|
||||
'maps',
|
||||
'unifiedHistogram',
|
||||
...(config.projectType === 'security' ? ['securitySolution'] : []),
|
||||
'unifiedSearch',
|
||||
]);
|
||||
// Validate individual plugin bundle sizes
|
||||
|
|
|
@ -0,0 +1,492 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
import { Readable } from 'stream';
|
||||
import supertest from 'supertest';
|
||||
|
||||
jest.mock('../../../../lib/content_stream', () => ({
|
||||
getContentStream: jest.fn(),
|
||||
}));
|
||||
|
||||
import { setupServer } from '@kbn/core-test-helpers-test-utils';
|
||||
import {
|
||||
ElasticsearchClientMock,
|
||||
coreMock,
|
||||
elasticsearchServiceMock,
|
||||
loggingSystemMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
|
||||
import { INTERNAL_ROUTES } from '@kbn/reporting-common';
|
||||
import { createMockConfigSchema } from '@kbn/reporting-mocks-server';
|
||||
import { ExportType } from '@kbn/reporting-server';
|
||||
import { ExportTypesRegistry } from '@kbn/reporting-server/export_types_registry';
|
||||
import { IUsageCounter } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counter';
|
||||
|
||||
import { ReportingCore } from '../../../..';
|
||||
import { ReportingInternalSetup, ReportingInternalStart } from '../../../../core';
|
||||
import { ContentStream, getContentStream } from '../../../../lib';
|
||||
import { reportingMock } from '../../../../mocks';
|
||||
import {
|
||||
createMockPluginSetup,
|
||||
createMockPluginStart,
|
||||
createMockReportingCore,
|
||||
} from '../../../../test_helpers';
|
||||
import { ReportingRequestHandlerContext, ScheduledReportType } from '../../../../types';
|
||||
import { EventTracker } from '../../../../usage';
|
||||
import { registerScheduledRoutesInternal } from '../scheduled';
|
||||
import {
|
||||
KibanaRequest,
|
||||
SavedObject,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
|
||||
|
||||
type SetupServerReturn = Awaited<ReturnType<typeof setupServer>>;
|
||||
|
||||
const fakeRawRequest = {
|
||||
headers: {
|
||||
authorization: `ApiKey skdjtq4u543yt3rhewrh`,
|
||||
},
|
||||
path: '/',
|
||||
} as unknown as KibanaRequest;
|
||||
|
||||
const payload =
|
||||
'{"browserTimezone":"America/New_York","layout":{"dimensions":{"height":2220,"width":1364},"id":"preserve_layout"},"objectType":"dashboard","title":"[Logs] Web Traffic","version":"9.1.0","locatorParams":[{"id":"DASHBOARD_APP_LOCATOR","params":{"dashboardId":"edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b","preserveSavedFilters":true,"timeRange":{"from":"now-7d/d","to":"now"},"useHash":false,"viewMode":"view"}}],"isDeprecated":false}';
|
||||
const jsonPayload = JSON.parse(payload);
|
||||
const savedObjects: Array<SavedObject<ScheduledReportType>> = [
|
||||
{
|
||||
type: 'scheduled_report',
|
||||
id: 'aa8b6fb3-cf61-4903-bce3-eec9ddc823ca',
|
||||
namespaces: ['a-space'],
|
||||
attributes: {
|
||||
createdAt: '2025-05-06T21:10:17.137Z',
|
||||
createdBy: 'elastic',
|
||||
enabled: true,
|
||||
jobType: 'printable_pdf_v2',
|
||||
meta: {
|
||||
isDeprecated: false,
|
||||
layout: 'preserve_layout',
|
||||
objectType: 'dashboard',
|
||||
},
|
||||
migrationVersion: '9.1.0',
|
||||
title: '[Logs] Web Traffic',
|
||||
payload,
|
||||
schedule: {
|
||||
rrule: {
|
||||
freq: 3,
|
||||
interval: 3,
|
||||
byhour: [12],
|
||||
byminute: [0],
|
||||
tzid: 'UTC',
|
||||
},
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
managed: false,
|
||||
updated_at: '2025-05-06T21:10:17.137Z',
|
||||
created_at: '2025-05-06T21:10:17.137Z',
|
||||
version: 'WzEsMV0=',
|
||||
coreMigrationVersion: '8.8.0',
|
||||
typeMigrationVersion: '10.1.0',
|
||||
},
|
||||
{
|
||||
type: 'scheduled_report',
|
||||
id: '2da1cb75-04c7-4202-a9f0-f8bcce63b0f4',
|
||||
namespaces: ['a-space'],
|
||||
attributes: {
|
||||
createdAt: '2025-05-06T21:12:06.584Z',
|
||||
createdBy: 'Tom Riddle',
|
||||
enabled: true,
|
||||
jobType: 'PNGV2',
|
||||
meta: {
|
||||
isDeprecated: false,
|
||||
layout: 'preserve_layout',
|
||||
objectType: 'dashboard',
|
||||
},
|
||||
migrationVersion: '9.1.0',
|
||||
notification: {
|
||||
email: {
|
||||
to: ['user@elastic.co'],
|
||||
},
|
||||
},
|
||||
title: 'Another cool dashboard',
|
||||
payload:
|
||||
'{"browserTimezone":"America/New_York","layout":{"dimensions":{"height":2220,"width":1364},"id":"preserve_layout"},"objectType":"dashboard","title":"[Logs] Web Traffic","version":"9.1.0","locatorParams":[{"id":"DASHBOARD_APP_LOCATOR","params":{"dashboardId":"edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b","preserveSavedFilters":true,"timeRange":{"from":"now-7d/d","to":"now"},"useHash":false,"viewMode":"view"}}],"isDeprecated":false}',
|
||||
schedule: {
|
||||
rrule: {
|
||||
freq: 1,
|
||||
interval: 3,
|
||||
tzid: 'UTC',
|
||||
},
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
managed: false,
|
||||
updated_at: '2025-05-06T21:12:06.584Z',
|
||||
created_at: '2025-05-06T21:12:06.584Z',
|
||||
version: 'WzIsMV0=',
|
||||
coreMigrationVersion: '8.8.0',
|
||||
typeMigrationVersion: '10.1.0',
|
||||
},
|
||||
];
|
||||
const soResponse: SavedObjectsFindResponse<ScheduledReportType> = {
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
total: 2,
|
||||
saved_objects: savedObjects.map((so) => ({ ...so, score: 0 })),
|
||||
};
|
||||
|
||||
const auditLogger = auditLoggerMock.create();
|
||||
|
||||
describe(`Reporting Schedule Management Routes: Internal`, () => {
|
||||
const reportingSymbol = Symbol('reporting');
|
||||
let server: SetupServerReturn['server'];
|
||||
let eventTracker: EventTracker;
|
||||
let usageCounter: IUsageCounter;
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
let exportTypesRegistry: ExportTypesRegistry;
|
||||
let reportingCore: ReportingCore;
|
||||
let mockSetupDeps: ReportingInternalSetup;
|
||||
let mockStartDeps: ReportingInternalStart;
|
||||
let mockEsClient: ElasticsearchClientMock;
|
||||
let stream: jest.Mocked<ContentStream>;
|
||||
let soClient: SavedObjectsClientContract;
|
||||
let client: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>;
|
||||
let taskManager: TaskManagerStartContract;
|
||||
|
||||
const mockLogger = loggingSystemMock.createLogger();
|
||||
const mockJobTypeUnencoded = 'unencodedJobType';
|
||||
const mockJobTypeBase64Encoded = 'base64EncodedJobType';
|
||||
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
const mockConfigSchema = createMockConfigSchema();
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, httpSetup } = await setupServer(reportingSymbol));
|
||||
httpSetup.registerRouteHandlerContext<ReportingRequestHandlerContext, 'reporting'>(
|
||||
reportingSymbol,
|
||||
'reporting',
|
||||
() => reportingMock.createStart()
|
||||
);
|
||||
|
||||
mockSetupDeps = createMockPluginSetup({
|
||||
security: {
|
||||
license: { isEnabled: () => true },
|
||||
},
|
||||
router: httpSetup.createRouter(''),
|
||||
});
|
||||
|
||||
mockStartDeps = await createMockPluginStart(
|
||||
{
|
||||
licensing: {
|
||||
...licensingMock.createStart(),
|
||||
license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }),
|
||||
},
|
||||
securityService: {
|
||||
authc: {
|
||||
getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }),
|
||||
},
|
||||
audit: {
|
||||
asScoped: () => auditLogger,
|
||||
},
|
||||
},
|
||||
},
|
||||
mockConfigSchema
|
||||
);
|
||||
|
||||
reportingCore = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps);
|
||||
|
||||
soClient = await reportingCore.getScopedSoClient(fakeRawRequest);
|
||||
soClient.find = jest.fn().mockImplementation(async () => {
|
||||
return soResponse;
|
||||
});
|
||||
|
||||
soClient.bulkGet = jest
|
||||
.fn()
|
||||
.mockImplementation(async () => ({ saved_objects: [savedObjects[1]] }));
|
||||
soClient.bulkUpdate = jest.fn().mockImplementation(async () => ({
|
||||
saved_objects: [savedObjects[1]].map((so) => ({
|
||||
id: so.id,
|
||||
type: so.type,
|
||||
attributes: { enabled: false },
|
||||
})),
|
||||
}));
|
||||
|
||||
taskManager = await reportingCore.getTaskManager();
|
||||
taskManager.bulkDisable = jest.fn().mockImplementation(async () => ({
|
||||
tasks: [savedObjects[1]].map((so) => ({ id: so.id })),
|
||||
errors: [],
|
||||
}));
|
||||
|
||||
client = (await reportingCore.getEsClient()).asInternalUser as typeof client;
|
||||
client.search.mockResponse({
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: { total: 1, successful: 1, skipped: 0, failed: 0 },
|
||||
hits: { total: { value: 0, relation: 'eq' }, max_score: null, hits: [] },
|
||||
});
|
||||
|
||||
usageCounter = {
|
||||
domainId: 'abc123',
|
||||
incrementCounter: jest.fn(),
|
||||
};
|
||||
jest.spyOn(reportingCore, 'getUsageCounter').mockReturnValue(usageCounter);
|
||||
|
||||
eventTracker = new EventTracker(coreSetupMock.analytics, 'jobId', 'exportTypeId', 'appId');
|
||||
jest.spyOn(reportingCore, 'getEventTracker').mockReturnValue(eventTracker);
|
||||
|
||||
exportTypesRegistry = new ExportTypesRegistry();
|
||||
exportTypesRegistry.register({
|
||||
id: 'unencoded',
|
||||
jobType: mockJobTypeUnencoded,
|
||||
jobContentExtension: 'csv',
|
||||
validLicenses: ['basic', 'gold'],
|
||||
} as ExportType);
|
||||
exportTypesRegistry.register({
|
||||
id: 'base64Encoded',
|
||||
jobType: mockJobTypeBase64Encoded,
|
||||
jobContentEncoding: 'base64',
|
||||
jobContentExtension: 'pdf',
|
||||
validLicenses: ['basic', 'gold'],
|
||||
} as ExportType);
|
||||
reportingCore.getExportTypesRegistry = () => exportTypesRegistry;
|
||||
|
||||
mockEsClient = (await reportingCore.getEsClient()).asInternalUser as typeof mockEsClient;
|
||||
stream = new Readable({
|
||||
read() {
|
||||
this.push('test');
|
||||
this.push(null);
|
||||
},
|
||||
}) as typeof stream;
|
||||
stream.end = jest.fn().mockImplementation((_name, _encoding, callback) => {
|
||||
callback();
|
||||
});
|
||||
|
||||
(getContentStream as jest.MockedFunction<typeof getContentStream>).mockResolvedValue(stream);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
describe('list scheduled reports', () => {
|
||||
it('correct lists scheduled reports', async () => {
|
||||
registerScheduledRoutesInternal(reportingCore, mockLogger);
|
||||
|
||||
await server.start();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get(`${INTERNAL_ROUTES.SCHEDULED.LIST}`)
|
||||
.expect(200)
|
||||
.then(({ body }) =>
|
||||
expect(body).toEqual({
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
total: 2,
|
||||
data: [
|
||||
{
|
||||
id: 'aa8b6fb3-cf61-4903-bce3-eec9ddc823ca',
|
||||
created_at: '2025-05-06T21:10:17.137Z',
|
||||
created_by: 'elastic',
|
||||
enabled: true,
|
||||
jobtype: 'printable_pdf_v2',
|
||||
next_run: expect.any(String),
|
||||
payload: jsonPayload,
|
||||
schedule: {
|
||||
rrule: {
|
||||
freq: 3,
|
||||
interval: 3,
|
||||
byhour: [12],
|
||||
byminute: [0],
|
||||
tzid: 'UTC',
|
||||
},
|
||||
},
|
||||
space_id: 'a-space',
|
||||
title: '[Logs] Web Traffic',
|
||||
},
|
||||
{
|
||||
id: '2da1cb75-04c7-4202-a9f0-f8bcce63b0f4',
|
||||
created_at: '2025-05-06T21:12:06.584Z',
|
||||
created_by: 'Tom Riddle',
|
||||
enabled: true,
|
||||
jobtype: 'PNGV2',
|
||||
next_run: expect.any(String),
|
||||
notification: {
|
||||
email: {
|
||||
to: ['user@elastic.co'],
|
||||
},
|
||||
},
|
||||
payload: jsonPayload,
|
||||
space_id: 'a-space',
|
||||
title: 'Another cool dashboard',
|
||||
schedule: {
|
||||
rrule: {
|
||||
freq: 1,
|
||||
interval: 3,
|
||||
tzid: 'UTC',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('fails on unauthenticated users', async () => {
|
||||
mockStartDeps = await createMockPluginStart(
|
||||
{
|
||||
licensing: {
|
||||
...licensingMock.createStart(),
|
||||
license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }),
|
||||
},
|
||||
securityService: {
|
||||
authc: { getCurrentUser: () => undefined },
|
||||
audit: {
|
||||
asScoped: () => auditLogger,
|
||||
},
|
||||
}, // security comes from core here
|
||||
},
|
||||
mockConfigSchema
|
||||
);
|
||||
reportingCore = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps);
|
||||
registerScheduledRoutesInternal(reportingCore, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get(`${INTERNAL_ROUTES.SCHEDULED.LIST}`)
|
||||
.expect(401)
|
||||
.then(({ body }) =>
|
||||
expect(body.message).toMatchInlineSnapshot(`"Sorry, you aren't authenticated"`)
|
||||
);
|
||||
});
|
||||
|
||||
it('fails on insufficient license', async () => {
|
||||
mockStartDeps = await createMockPluginStart(
|
||||
{
|
||||
licensing: {
|
||||
...licensingMock.createStart(),
|
||||
license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'basic' }),
|
||||
},
|
||||
securityService: {
|
||||
authc: {
|
||||
getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }),
|
||||
},
|
||||
audit: {
|
||||
asScoped: () => auditLogger,
|
||||
},
|
||||
},
|
||||
},
|
||||
mockConfigSchema
|
||||
);
|
||||
reportingCore = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps);
|
||||
registerScheduledRoutesInternal(reportingCore, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get(`${INTERNAL_ROUTES.SCHEDULED.LIST}`)
|
||||
.expect(403)
|
||||
.then(({ body }) =>
|
||||
expect(body.message).toMatchInlineSnapshot(
|
||||
`"Your basic license does not support Scheduled reports. Please upgrade your license."`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable scheduled reports', () => {
|
||||
it('correct disables scheduled reports', async () => {
|
||||
registerScheduledRoutesInternal(reportingCore, mockLogger);
|
||||
|
||||
await server.start();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.patch(`${INTERNAL_ROUTES.SCHEDULED.BULK_DISABLE}`)
|
||||
.send({
|
||||
ids: ['2da1cb75-04c7-4202-a9f0-f8bcce63b0f4'],
|
||||
})
|
||||
.expect(200)
|
||||
.then(({ body }) =>
|
||||
expect(body).toEqual({
|
||||
total: 1,
|
||||
scheduled_report_ids: ['2da1cb75-04c7-4202-a9f0-f8bcce63b0f4'],
|
||||
errors: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('fails on unauthenticated users', async () => {
|
||||
mockStartDeps = await createMockPluginStart(
|
||||
{
|
||||
licensing: {
|
||||
...licensingMock.createStart(),
|
||||
license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }),
|
||||
},
|
||||
securityService: {
|
||||
authc: { getCurrentUser: () => undefined },
|
||||
audit: {
|
||||
asScoped: () => auditLogger,
|
||||
},
|
||||
}, // security comes from core here
|
||||
},
|
||||
mockConfigSchema
|
||||
);
|
||||
reportingCore = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps);
|
||||
registerScheduledRoutesInternal(reportingCore, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.patch(`${INTERNAL_ROUTES.SCHEDULED.BULK_DISABLE}`)
|
||||
.send({
|
||||
ids: ['2da1cb75-04c7-4202-a9f0-f8bcce63b0f4'],
|
||||
})
|
||||
.expect(401)
|
||||
.then(({ body }) =>
|
||||
expect(body.message).toMatchInlineSnapshot(`"Sorry, you aren't authenticated"`)
|
||||
);
|
||||
});
|
||||
|
||||
it('fails on insufficient license', async () => {
|
||||
mockStartDeps = await createMockPluginStart(
|
||||
{
|
||||
licensing: {
|
||||
...licensingMock.createStart(),
|
||||
license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'basic' }),
|
||||
},
|
||||
securityService: {
|
||||
authc: {
|
||||
getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }),
|
||||
},
|
||||
audit: {
|
||||
asScoped: () => auditLogger,
|
||||
},
|
||||
},
|
||||
},
|
||||
mockConfigSchema
|
||||
);
|
||||
reportingCore = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps);
|
||||
registerScheduledRoutesInternal(reportingCore, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.patch(`${INTERNAL_ROUTES.SCHEDULED.BULK_DISABLE}`)
|
||||
.send({
|
||||
ids: ['2da1cb75-04c7-4202-a9f0-f8bcce63b0f4'],
|
||||
})
|
||||
.expect(403)
|
||||
.then(({ body }) =>
|
||||
expect(body.message).toMatchInlineSnapshot(
|
||||
`"Your basic license does not support Scheduled reports. Please upgrade your license."`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -67,6 +67,14 @@ export function registerScheduledRoutesInternal(reporting: ReportingCore, logger
|
|||
return handleUnavailable(res);
|
||||
}
|
||||
|
||||
// check license
|
||||
const licenseInfo = await reporting.getLicenseInfo();
|
||||
const licenseResults = licenseInfo.scheduledReports;
|
||||
|
||||
if (!licenseResults.enableLinks) {
|
||||
return res.forbidden({ body: licenseResults.message });
|
||||
}
|
||||
|
||||
const {
|
||||
page: queryPage = '1',
|
||||
size: querySize = `${DEFAULT_SCHEDULED_REPORT_LIST_SIZE}`,
|
||||
|
@ -120,6 +128,14 @@ export function registerScheduledRoutesInternal(reporting: ReportingCore, logger
|
|||
return handleUnavailable(res);
|
||||
}
|
||||
|
||||
// check license
|
||||
const licenseInfo = await reporting.getLicenseInfo();
|
||||
const licenseResults = licenseInfo.scheduledReports;
|
||||
|
||||
if (!licenseResults.enableLinks) {
|
||||
return res.forbidden({ body: licenseResults.message });
|
||||
}
|
||||
|
||||
const { ids } = req.body;
|
||||
|
||||
const results = await scheduledQuery.bulkDisable(logger, req, res, ids, user);
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
title: Connector request properties for an OpenAI connector with Other provider
|
||||
description: >
|
||||
Defines properties for connectors when type is `.gen-ai` and the API provider is `Other` (OpenAI-compatible service), including optional PKI authentication.
|
||||
type: object
|
||||
required:
|
||||
- apiProvider
|
||||
- apiUrl
|
||||
- defaultModel
|
||||
properties:
|
||||
apiProvider:
|
||||
type: string
|
||||
description: The OpenAI API provider.
|
||||
enum: ['Other']
|
||||
apiUrl:
|
||||
type: string
|
||||
description: The OpenAI-compatible API endpoint.
|
||||
defaultModel:
|
||||
type: string
|
||||
description: The default model to use for requests.
|
||||
certificateData:
|
||||
type: string
|
||||
description: PEM-encoded certificate content.
|
||||
minLength: 1
|
||||
privateKeyData:
|
||||
type: string
|
||||
description: PEM-encoded private key content.
|
||||
minLength: 1
|
||||
caData:
|
||||
type: string
|
||||
description: PEM-encoded CA certificate content.
|
||||
minLength: 1
|
||||
verificationMode:
|
||||
type: string
|
||||
description: SSL verification mode for PKI authentication.
|
||||
enum: ['full', 'certificate', 'none']
|
||||
default: 'full'
|
||||
headers:
|
||||
type: object
|
||||
description: Custom headers to include in requests.
|
||||
additionalProperties:
|
||||
type: string
|
|
@ -1,7 +1,26 @@
|
|||
title: Connector secrets properties for an OpenAI connector
|
||||
description: Defines secrets for connectors when type is `.gen-ai`.
|
||||
# Defines secrets for connectors when type is `.gen-ai`, including support for PKI authentication for 'Other' providers.
|
||||
description: |
|
||||
Defines secrets for connectors when type is `.gen-ai`. Supports both API key authentication (OpenAI, Azure OpenAI, and `Other`) and PKI authentication (`Other` provider only). PKI fields must be base64-encoded PEM content.
|
||||
type: object
|
||||
properties:
|
||||
apiKey:
|
||||
type: string
|
||||
description: The OpenAI API key.
|
||||
apiKey:
|
||||
type: string
|
||||
description: |
|
||||
The API key for authentication. For OpenAI and Azure OpenAI providers, it is required. For the `Other` provider, it is required if you do not use PKI authentication. With PKI, you can also optionally include an API key if the OpenAI-compatible service supports or requires one.
|
||||
certificateData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM certificate content for PKI authentication (Other provider only). Required for PKI.
|
||||
minLength: 1
|
||||
privateKeyData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM private key content for PKI authentication (Other provider only). Required for PKI.
|
||||
minLength: 1
|
||||
caData:
|
||||
type: string
|
||||
description: |
|
||||
Base64-encoded PEM CA certificate content for PKI authentication (Other provider only). Optional.
|
||||
minLength: 1
|
||||
required: []
|
||||
|
|
|
@ -24,6 +24,7 @@ export type {
|
|||
ActionType,
|
||||
InMemoryConnector,
|
||||
ActionsApiRequestHandlerContext,
|
||||
SSLSettings,
|
||||
} from './types';
|
||||
|
||||
export type { ConnectorWithExtraFindData as FindActionResult } from './application/connector/types';
|
||||
|
|
|
@ -2763,7 +2763,7 @@ Object {
|
|||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"limit": 10,
|
||||
"limit": 20,
|
||||
},
|
||||
"name": "max",
|
||||
},
|
||||
|
@ -2870,7 +2870,7 @@ Object {
|
|||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"limit": 10,
|
||||
"limit": 20,
|
||||
},
|
||||
"name": "max",
|
||||
},
|
||||
|
|
|
@ -71,7 +71,8 @@ export function getCustomAgents(
|
|||
// This is where the global rejectUnauthorized is overridden by a custom host
|
||||
const customHostNodeSSLOptions = getNodeSSLOptions(
|
||||
logger,
|
||||
sslSettingsFromConfig.verificationMode
|
||||
sslSettingsFromConfig.verificationMode,
|
||||
sslOverrides
|
||||
);
|
||||
if (customHostNodeSSLOptions.rejectUnauthorized !== undefined) {
|
||||
agentOptions.rejectUnauthorized = customHostNodeSSLOptions.rejectUnauthorized;
|
||||
|
@ -119,7 +120,8 @@ export function getCustomAgents(
|
|||
|
||||
const proxyNodeSSLOptions = getNodeSSLOptions(
|
||||
logger,
|
||||
proxySettings.proxySSLSettings.verificationMode
|
||||
proxySettings.proxySSLSettings.verificationMode,
|
||||
sslOverrides
|
||||
);
|
||||
// At this point, we are going to use a proxy, so we need new agents.
|
||||
// We will though, copy over the calculated ssl options from above, into
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
ActionTypeParams,
|
||||
RenderParameterTemplates,
|
||||
Services,
|
||||
SSLSettings,
|
||||
ValidatorType as ValidationSchema,
|
||||
} from '../types';
|
||||
import type { SubFeature } from '../../common';
|
||||
|
@ -41,6 +42,7 @@ export type SubActionRequestParams<R> = {
|
|||
url: string;
|
||||
responseSchema: Type<R>;
|
||||
method?: Method;
|
||||
sslOverrides?: SSLSettings;
|
||||
} & AxiosRequestConfig;
|
||||
|
||||
export type IService<Config, Secrets> = new (
|
||||
|
|
|
@ -324,6 +324,23 @@ describe('mappingFromFieldMap', () => {
|
|||
type: 'date_range',
|
||||
format: 'epoch_millis||strict_date_optional_time',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
updated_by: {
|
||||
properties: {
|
||||
user: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
url: {
|
||||
ignore_above: 2048,
|
||||
index: false,
|
||||
|
|
|
@ -1477,6 +1477,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -2615,6 +2630,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -3753,6 +3783,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -4891,6 +4936,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -6029,6 +6089,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -7173,6 +7248,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -8311,6 +8401,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -9449,6 +9554,21 @@ Object {
|
|||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
|
@ -9979,6 +10099,253 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: streams.rules.esql 1`] = `
|
||||
Object {
|
||||
"dynamic": false,
|
||||
"fieldMap": Object {
|
||||
"@timestamp": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "date",
|
||||
},
|
||||
"event.action": Object {
|
||||
"array": false,
|
||||
"ignore_above": 1024,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"event.kind": Object {
|
||||
"array": false,
|
||||
"ignore_above": 1024,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"event.original": Object {
|
||||
"array": false,
|
||||
"ignore_above": 1024,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.action_group": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.case_ids": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.consecutive_matches": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "long",
|
||||
},
|
||||
"kibana.alert.duration.us": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "long",
|
||||
},
|
||||
"kibana.alert.end": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.flapping": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"kibana.alert.flapping_history": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"kibana.alert.instance.id": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.intended_timestamp": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.last_detected": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.maintenance_window_ids": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.pending_recovered_count": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "long",
|
||||
},
|
||||
"kibana.alert.previous_action_group": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.reason": Object {
|
||||
"array": false,
|
||||
"multi_fields": Array [
|
||||
Object {
|
||||
"flat_name": "kibana.alert.reason.text",
|
||||
"name": "text",
|
||||
"type": "match_only_text",
|
||||
},
|
||||
],
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.category": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.consumer": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.execution.timestamp": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.rule.execution.type": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.execution.uuid": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.name": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.parameters": Object {
|
||||
"array": false,
|
||||
"ignore_above": 4096,
|
||||
"required": false,
|
||||
"type": "flattened",
|
||||
},
|
||||
"kibana.alert.rule.producer": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.revision": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "long",
|
||||
},
|
||||
"kibana.alert.rule.rule_type_id": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.rule.uuid": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.severity_improving": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "boolean",
|
||||
},
|
||||
"kibana.alert.start": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.status": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.time_range": Object {
|
||||
"array": false,
|
||||
"format": "epoch_millis||strict_date_optional_time",
|
||||
"required": false,
|
||||
"type": "date_range",
|
||||
},
|
||||
"kibana.alert.updated_at": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "date",
|
||||
},
|
||||
"kibana.alert.updated_by.user.id": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.updated_by.user.name": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.url": Object {
|
||||
"array": false,
|
||||
"ignore_above": 2048,
|
||||
"index": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.uuid": Object {
|
||||
"array": false,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.workflow_assignee_ids": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.workflow_status": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.alert.workflow_tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.space_ids": Object {
|
||||
"array": true,
|
||||
"required": true,
|
||||
"type": "keyword",
|
||||
},
|
||||
"kibana.version": Object {
|
||||
"array": false,
|
||||
"required": false,
|
||||
"type": "version",
|
||||
},
|
||||
"tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Alert as data fields checks detect AAD fields changes for: transform_health 1`] = `
|
||||
Object {
|
||||
"fieldMap": Object {
|
||||
|
|
|
@ -27,6 +27,7 @@ const ruleTypes: string[] = [
|
|||
'xpack.ml.anomaly_detection_alert',
|
||||
'xpack.ml.anomaly_detection_jobs_health',
|
||||
'slo.rules.burnRate',
|
||||
'streams.rules.esql',
|
||||
'observability.rules.custom_threshold',
|
||||
'xpack.uptime.alerts.monitorStatus',
|
||||
'xpack.uptime.alerts.tlsCertificate',
|
||||
|
|
|
@ -27,6 +27,12 @@ export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure' as const;
|
|||
export const CASE_RULES_SAVED_OBJECT = 'cases-rules' as const;
|
||||
export const CASE_ID_INCREMENTER_SAVED_OBJECT = 'cases-incrementing-id' as const;
|
||||
|
||||
/**
|
||||
* UI settings
|
||||
*/
|
||||
|
||||
export const CASES_UI_SETTING_ID_DISPLAY_INCREMENTAL_ID = 'cases:incrementalIdDisplay:enabled';
|
||||
|
||||
/**
|
||||
* If more values are added here please also add them here: x-pack/test/cases_api_integration/common/plugins
|
||||
*/
|
||||
|
|
|
@ -246,6 +246,7 @@ export const BulkCreateCasesResponseRt = rt.strict({
|
|||
export const CasesFindRequestSearchFieldsRt = rt.keyof({
|
||||
description: null,
|
||||
title: null,
|
||||
incremental_id: null,
|
||||
});
|
||||
|
||||
export const CasesFindRequestSortFieldsRt = rt.keyof({
|
||||
|
|
|
@ -72,6 +72,9 @@ export interface CasesUiConfigType {
|
|||
stack: {
|
||||
enabled: boolean;
|
||||
};
|
||||
incrementalId: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const UserActionTypeAll = 'all' as const;
|
||||
|
@ -342,3 +345,7 @@ export interface CasesCapabilities {
|
|||
[CASES_REOPEN_CAPABILITY]: boolean;
|
||||
[ASSIGN_CASE_CAPABILITY]: boolean;
|
||||
}
|
||||
|
||||
export interface CasesSettings {
|
||||
displayIncrementalCaseId: boolean;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
|||
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { BaseFilesClient } from '@kbn/shared-ux-file-types';
|
||||
import type { CasesFeatures, CasesPermissions } from '../../../common/ui/types';
|
||||
import type { CasesFeatures, CasesPermissions, CasesSettings } from '../../../common/ui/types';
|
||||
import type { ReleasePhase } from '../../components/types';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import type { CasesContextProps } from '../../components/cases_context';
|
||||
|
@ -47,6 +47,7 @@ interface TestProviderProps {
|
|||
queryClient?: QueryClient;
|
||||
coreStart?: CoreStart;
|
||||
filesClient?: BaseFilesClient;
|
||||
settings?: CasesSettings;
|
||||
}
|
||||
|
||||
window.scrollTo = jest.fn();
|
||||
|
@ -92,6 +93,7 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
|
|||
services,
|
||||
queryClient,
|
||||
filesClient,
|
||||
settings,
|
||||
}) => {
|
||||
const finalCoreStart = useMemo(() => coreStart ?? coreMock.createStart(), [coreStart]);
|
||||
const finalServices = useMemo(
|
||||
|
@ -138,6 +140,9 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
|
|||
permissions: permissions ?? defaultPermissions,
|
||||
releasePhase: releasePhase ?? 'ga',
|
||||
getFilesClient: getFilesClientFinal,
|
||||
settings: settings ?? {
|
||||
displayIncrementalCaseId: false,
|
||||
},
|
||||
}),
|
||||
[
|
||||
defaultExternalReferenceAttachmentTypeRegistry,
|
||||
|
@ -150,6 +155,7 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
|
|||
permissions,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
releasePhase,
|
||||
settings,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -144,6 +144,9 @@ describe('AllCasesListGeneric', () => {
|
|||
userProfiles: new Map(),
|
||||
currentUserProfile: undefined,
|
||||
selectedColumns: [],
|
||||
settings: {
|
||||
displayIncrementalCaseId: false,
|
||||
},
|
||||
};
|
||||
|
||||
const removeMsFromDate = (value: string) => moment(value).format('YYYY-MM-DDTHH:mm:ss[Z]');
|
||||
|
@ -151,7 +154,10 @@ describe('AllCasesListGeneric', () => {
|
|||
beforeAll(() => {
|
||||
patchGetComputedStyle();
|
||||
mockKibana();
|
||||
const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry;
|
||||
const {
|
||||
triggersActionsUi: { actionTypeRegistry },
|
||||
} = useKibanaMock().services;
|
||||
|
||||
registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock);
|
||||
});
|
||||
|
||||
|
@ -183,7 +189,9 @@ describe('AllCasesListGeneric', () => {
|
|||
|
||||
it('should render AllCasesList', async () => {
|
||||
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true });
|
||||
renderWithTestingProviders(<AllCasesList />);
|
||||
renderWithTestingProviders(<AllCasesList />, {
|
||||
wrapperProps: { settings: { displayIncrementalCaseId: true } },
|
||||
});
|
||||
|
||||
const caseDetailsLinks = await screen.findAllByTestId('case-details-link');
|
||||
|
||||
|
@ -193,6 +201,10 @@ describe('AllCasesListGeneric', () => {
|
|||
(await screen.findAllByTestId('case-user-profile-avatar-damaged_raccoon'))[0]
|
||||
).toHaveTextContent('DR');
|
||||
|
||||
const incrementalIdTextElements = screen.getAllByTestId('cases-incremental-id-text');
|
||||
expect(incrementalIdTextElements).toHaveLength(1);
|
||||
expect(incrementalIdTextElements[0]).toHaveTextContent('#1');
|
||||
|
||||
expect((await screen.findAllByTestId('case-table-column-tags-coke'))[0]).toHaveAttribute(
|
||||
'title',
|
||||
useGetCasesMockState.data.cases[0].tags[0]
|
||||
|
@ -214,6 +226,18 @@ describe('AllCasesListGeneric', () => {
|
|||
expect(screen.queryByTestId('all-cases-clear-filters-link-icon')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render incremental id if setting is disabled', async () => {
|
||||
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true });
|
||||
renderWithTestingProviders(<AllCasesList />, {
|
||||
wrapperProps: { settings: { displayIncrementalCaseId: false } },
|
||||
});
|
||||
|
||||
await screen.findAllByTestId('case-details-link');
|
||||
|
||||
const incrementalIdTextElements = screen.queryAllByTestId('cases-incremental-id-text');
|
||||
expect(incrementalIdTextElements).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should show a tooltip with the assignee's email when hover over the assignee avatar", async () => {
|
||||
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true });
|
||||
|
||||
|
@ -515,7 +539,6 @@ describe('AllCasesListGeneric', () => {
|
|||
expect(useGetCasesMock).toHaveBeenLastCalledWith({
|
||||
filterOptions: {
|
||||
...DEFAULT_FILTER_OPTIONS,
|
||||
searchFields: ['title', 'description'],
|
||||
category: ['twix'],
|
||||
},
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
|
|
|
@ -46,7 +46,7 @@ export interface AllCasesListProps {
|
|||
|
||||
export const AllCasesList = React.memo<AllCasesListProps>(
|
||||
({ hiddenStatuses = [], isSelectorView = false, onRowClick }) => {
|
||||
const { owner, permissions } = useCasesContext();
|
||||
const { owner, permissions, settings } = useCasesContext();
|
||||
|
||||
const availableSolutions = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete'));
|
||||
const isLoading = useIsLoadingCases();
|
||||
|
@ -141,6 +141,7 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
onRowClick,
|
||||
disableActions: selectedCases.length > 0,
|
||||
selectedColumns,
|
||||
settings,
|
||||
});
|
||||
|
||||
const pagination = useMemo(
|
||||
|
|
|
@ -79,6 +79,7 @@ describe('use cases add to existing case modal hook', () => {
|
|||
observables: { enabled: true },
|
||||
},
|
||||
releasePhase: 'ga',
|
||||
settings: { displayIncrementalCaseId: false },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
|
||||
import type { GetCasesColumn } from './use_cases_columns';
|
||||
|
@ -46,6 +45,7 @@ describe('useCasesColumns ', () => {
|
|||
userProfiles: userProfilesMap,
|
||||
isSelectorView: false,
|
||||
selectedColumns: DEFAULT_SELECTED_COLUMNS,
|
||||
settings: { displayIncrementalCaseId: true },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -630,70 +630,56 @@ describe('useCasesColumns ', () => {
|
|||
|
||||
describe('ExternalServiceColumn ', () => {
|
||||
it('Not pushed render', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[0]}
|
||||
connectors={connectors}
|
||||
/>
|
||||
</TestProviders>
|
||||
renderWithTestingProviders(
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[0]}
|
||||
connectors={connectors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-table-column-external-notPushed"]`).last().exists()
|
||||
).toBeTruthy();
|
||||
expect(screen.getByTestId('case-table-column-external-notPushed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Up to date', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[1]}
|
||||
connectors={connectors}
|
||||
/>
|
||||
</TestProviders>
|
||||
renderWithTestingProviders(
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[1]}
|
||||
connectors={connectors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-table-column-external-upToDate"]`).last().exists()
|
||||
).toBeTruthy();
|
||||
expect(screen.getByTestId('case-table-column-external-upToDate')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Needs update', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[2]}
|
||||
connectors={connectors}
|
||||
/>
|
||||
</TestProviders>
|
||||
renderWithTestingProviders(
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[2]}
|
||||
connectors={connectors}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-table-column-external-requiresUpdate"]`).last().exists()
|
||||
).toBeTruthy();
|
||||
expect(screen.getByTestId('case-table-column-external-requiresUpdate')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('it does not throw when accessing the icon if the connector type is not registered', () => {
|
||||
// If the component throws the test will fail
|
||||
expect(() =>
|
||||
mount(
|
||||
<TestProviders>
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[2]}
|
||||
connectors={[
|
||||
{
|
||||
id: 'none',
|
||||
actionTypeId: '.none',
|
||||
name: 'None',
|
||||
config: {},
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TestProviders>
|
||||
renderWithTestingProviders(
|
||||
<ExternalServiceColumn
|
||||
theCase={useGetCasesMockState.data.cases[2]}
|
||||
connectors={[
|
||||
{
|
||||
id: 'none',
|
||||
actionTypeId: '.none',
|
||||
name: 'None',
|
||||
config: {},
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
|
|||
|
||||
import type { ActionConnector } from '../../../common/types/domain';
|
||||
import { CaseSeverity } from '../../../common/types/domain';
|
||||
import type { CaseUI } from '../../../common/ui/types';
|
||||
import type { CaseUI, CasesSettings } from '../../../common/ui/types';
|
||||
import type { CasesColumnSelection } from './types';
|
||||
import { getEmptyCellValue } from '../empty_value';
|
||||
import { FormattedRelativePreferenceDate } from '../formatted_date';
|
||||
|
@ -42,6 +42,7 @@ import { severities } from '../severity/config';
|
|||
import { AssigneesColumn } from './assignees_column';
|
||||
import { builderMap as customFieldsBuilderMap } from '../custom_fields/builder';
|
||||
import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
|
||||
import { IncrementalIdText } from '../incremental_id';
|
||||
|
||||
type CasesColumns =
|
||||
| EuiTableActionsColumnType<CaseUI>
|
||||
|
@ -66,6 +67,7 @@ export interface GetCasesColumn {
|
|||
userProfiles: Map<string, UserProfileWithAvatar>;
|
||||
isSelectorView: boolean;
|
||||
selectedColumns: CasesColumnSelection[];
|
||||
settings: CasesSettings;
|
||||
connectors?: ActionConnector[];
|
||||
onRowClick?: (theCase: CaseUI) => void;
|
||||
disableActions?: boolean;
|
||||
|
@ -84,6 +86,7 @@ export const useCasesColumns = ({
|
|||
onRowClick,
|
||||
disableActions = false,
|
||||
selectedColumns,
|
||||
settings,
|
||||
}: GetCasesColumn): UseCasesColumnsReturnValue => {
|
||||
const casesColumnsConfig = useCasesColumnsConfiguration(isSelectorView);
|
||||
const { actions } = useActions({ disableActions });
|
||||
|
@ -113,9 +116,14 @@ export const useCasesColumns = ({
|
|||
const caseDetailsLinkComponent = isSelectorView ? (
|
||||
theCase.title
|
||||
) : (
|
||||
<CaseDetailsLink detailName={theCase.id} title={theCase.title}>
|
||||
<TruncatedText text={theCase.title} />
|
||||
</CaseDetailsLink>
|
||||
<div>
|
||||
<CaseDetailsLink detailName={theCase.id} title={theCase.title}>
|
||||
<TruncatedText text={theCase.title} />
|
||||
</CaseDetailsLink>
|
||||
{settings.displayIncrementalCaseId && typeof theCase.incrementalId === 'number' ? (
|
||||
<IncrementalIdText incrementalId={theCase.incrementalId} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return caseDetailsLinkComponent;
|
||||
|
@ -334,7 +342,7 @@ export const useCasesColumns = ({
|
|||
width: '120px',
|
||||
},
|
||||
}),
|
||||
[assignCaseAction, casesColumnsConfig, connectors, isSelectorView, userProfiles]
|
||||
[assignCaseAction, casesColumnsConfig, connectors, isSelectorView, userProfiles, settings]
|
||||
);
|
||||
|
||||
// we need to extend the columnsDict with the columns of
|
||||
|
|
|
@ -98,6 +98,7 @@ export const CaseViewPage = React.memo<CaseViewPageProps>(
|
|||
/>
|
||||
}
|
||||
title={caseData.title}
|
||||
incrementalId={caseData.incrementalId}
|
||||
>
|
||||
<CaseActionBar
|
||||
caseData={caseData}
|
||||
|
|
|
@ -19,13 +19,18 @@ import type {
|
|||
CasesFeaturesAllRequired,
|
||||
CasesFeatures,
|
||||
CasesPermissions,
|
||||
CasesSettings,
|
||||
} from '../../containers/types';
|
||||
import type { ReleasePhase } from '../types';
|
||||
import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry';
|
||||
|
||||
import { CasesGlobalComponents } from './cases_global_components';
|
||||
import { DEFAULT_FEATURES } from '../../../common/constants';
|
||||
import {
|
||||
CASES_UI_SETTING_ID_DISPLAY_INCREMENTAL_ID,
|
||||
DEFAULT_FEATURES,
|
||||
} from '../../../common/constants';
|
||||
import { KibanaServices, useKibana } from '../../common/lib/kibana';
|
||||
import { constructFileKindIdByOwner } from '../../../common/files';
|
||||
import { DEFAULT_BASE_PATH } from '../../common/navigation';
|
||||
import type { CasesContextStoreAction } from './state/cases_context_reducer';
|
||||
|
@ -45,6 +50,7 @@ export interface CasesContextValue {
|
|||
features: CasesFeaturesAllRequired;
|
||||
releasePhase: ReleasePhase;
|
||||
dispatch: CasesContextValueDispatch;
|
||||
settings: CasesSettings;
|
||||
}
|
||||
|
||||
export interface CasesContextProps
|
||||
|
@ -59,6 +65,7 @@ export interface CasesContextProps
|
|||
features?: CasesFeatures;
|
||||
releasePhase?: ReleasePhase;
|
||||
getFilesClient: (scope: string) => ScopedFilesClient;
|
||||
settings?: CasesContextValue['settings'];
|
||||
}
|
||||
|
||||
export const CasesContext = React.createContext<CasesContextValue | undefined>(undefined);
|
||||
|
@ -79,9 +86,20 @@ export const CasesProvider: FC<
|
|||
features = {},
|
||||
releasePhase = 'ga',
|
||||
getFilesClient,
|
||||
settings,
|
||||
},
|
||||
queryClient = casesQueryClient,
|
||||
}) => {
|
||||
const {
|
||||
settings: { client },
|
||||
} = useKibana().services;
|
||||
|
||||
// UI setting enablement is behind the configuration flag, so will error without this wrapper
|
||||
let displayIncrementalCaseId = false;
|
||||
if (KibanaServices.getConfig()?.incrementalId?.enabled) {
|
||||
displayIncrementalCaseId = client.get(CASES_UI_SETTING_ID_DISPLAY_INCREMENTAL_ID);
|
||||
}
|
||||
|
||||
const [state, dispatch] = useReducer(casesContextReducer, getInitialCasesContextState());
|
||||
|
||||
const value: CasesContextValue = useMemo(
|
||||
|
@ -114,6 +132,9 @@ export const CasesProvider: FC<
|
|||
),
|
||||
releasePhase,
|
||||
dispatch,
|
||||
settings: settings ?? {
|
||||
displayIncrementalCaseId,
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* We want to trigger a rerender only when the permissions will change.
|
||||
|
|
|
@ -42,6 +42,7 @@ describe('use cases add to new case flyout hook', () => {
|
|||
observables: { enabled: true },
|
||||
},
|
||||
releasePhase: 'ga',
|
||||
settings: { displayIncrementalCaseId: false },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -34,6 +34,26 @@ describe('HeaderPage', () => {
|
|||
expect(screen.getByText('Test supplement')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the `incremental_id` when provided', () => {
|
||||
renderWithTestingProviders(
|
||||
<TestProviders settings={{ displayIncrementalCaseId: true }}>
|
||||
<HeaderPage border title="Test title" incrementalId={1337} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByText('#1337')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the `incremental_id` when setting disabled', () => {
|
||||
renderWithTestingProviders(
|
||||
<TestProviders settings={{ displayIncrementalCaseId: false }}>
|
||||
<HeaderPage border title="Test title" incrementalId={1337} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('#1337')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('DOES NOT render the back link when not provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -12,6 +12,7 @@ import { css } from '@emotion/react';
|
|||
|
||||
import { Title } from './title';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { IncrementalIdText } from '../incremental_id';
|
||||
|
||||
interface HeaderProps {
|
||||
border?: boolean;
|
||||
|
@ -22,6 +23,7 @@ export interface HeaderPageProps extends HeaderProps {
|
|||
children?: React.ReactNode;
|
||||
title: string | React.ReactNode;
|
||||
titleNode?: React.ReactElement;
|
||||
incrementalId?: number | null;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
|
@ -43,14 +45,18 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
|
|||
isLoading,
|
||||
title,
|
||||
titleNode,
|
||||
incrementalId,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const { releasePhase } = useCasesContext();
|
||||
const { releasePhase, settings } = useCasesContext();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<header css={getHeaderCss(euiTheme, border)} data-test-subj={dataTestSubj}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
{settings.displayIncrementalCaseId && incrementalId && (
|
||||
<IncrementalIdText incrementalId={incrementalId} />
|
||||
)}
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
overflow: hidden;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderWithTestingProviders } from '../../common/mock';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { IncrementalIdText } from '.';
|
||||
|
||||
describe('IncrementalIdText', () => {
|
||||
it('renders the incremental id', () => {
|
||||
renderWithTestingProviders(<IncrementalIdText incrementalId={1337} />);
|
||||
expect(screen.getByTestId('cases-incremental-id-text')).toHaveTextContent('#1337');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
export const IncrementalIdText = React.memo<{ incrementalId: number }>(({ incrementalId }) => (
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="s"
|
||||
data-test-subj="cases-incremental-id-text"
|
||||
css={css`
|
||||
user-select: all;
|
||||
`}
|
||||
>
|
||||
{'#'}
|
||||
{incrementalId}
|
||||
</EuiText>
|
||||
));
|
||||
IncrementalIdText.displayName = 'IncrementalIdText';
|
|
@ -26,6 +26,9 @@ jest.mock('../../common/lib/kibana', () => {
|
|||
KibanaServices: {
|
||||
...originalModule.KibanaServices,
|
||||
get: () => mockGetKibanaServices(),
|
||||
getConfig: () => ({
|
||||
incrementalId: { enabled: true },
|
||||
}),
|
||||
},
|
||||
useNavigation: jest.fn().mockReturnValue({
|
||||
getAppUrl: jest.fn((params?: { deepLinkId: string }) => params?.deepLinkId ?? '/test'),
|
||||
|
|
|
@ -453,8 +453,15 @@ export const cases: CasesUI = [
|
|||
comments: [],
|
||||
status: CaseStatuses['in-progress'],
|
||||
severity: CaseSeverity.MEDIUM,
|
||||
incrementalId: 1,
|
||||
},
|
||||
{
|
||||
...pushedCase,
|
||||
updatedAt: laterTime,
|
||||
id: '2',
|
||||
totalComment: 0,
|
||||
comments: [],
|
||||
},
|
||||
{ ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] },
|
||||
{ ...basicCase, id: '3', totalComment: 0, comments: [] },
|
||||
{ ...basicCase, id: '4', totalComment: 0, comments: [] },
|
||||
caseWithAlerts,
|
||||
|
@ -621,6 +628,7 @@ export const casesSnake: Cases = [
|
|||
{
|
||||
...pushedCaseSnake,
|
||||
id: '1',
|
||||
incremental_id: 1,
|
||||
totalComment: 0,
|
||||
comments: [],
|
||||
status: CaseStatuses['in-progress'],
|
||||
|
|
|
@ -149,4 +149,29 @@ describe('useGetCases', () => {
|
|||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
it('should change search and searchFields for incremental id searches', async () => {
|
||||
const spyOnGetCases = jest.spyOn(api, 'getCases');
|
||||
|
||||
renderHook(() => useGetCases({ filterOptions: { search: '#123' } }), {
|
||||
wrapper: (props) => (
|
||||
<TestProviders {...props} settings={{ displayIncrementalCaseId: true }} />
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spyOnGetCases).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(spyOnGetCases).toBeCalledWith({
|
||||
filterOptions: {
|
||||
...DEFAULT_FILTER_OPTIONS,
|
||||
search: '123',
|
||||
searchFields: ['incremental_id'],
|
||||
owner: ['securitySolution'],
|
||||
},
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,8 @@ import { useCasesContext } from '../components/cases_context/use_cases_context';
|
|||
import { useAvailableCasesOwners } from '../components/app/use_available_owners';
|
||||
import { getAllPermissionsExceptFrom } from '../utils/permissions';
|
||||
|
||||
const incrementalIdRegEx = /^#(\d{1,50})\s*$/;
|
||||
|
||||
export const initialData: CasesFindResponseUI = {
|
||||
cases: [],
|
||||
countClosedCases: 0,
|
||||
|
@ -34,7 +36,7 @@ export const useGetCases = (
|
|||
} = {}
|
||||
): UseQueryResult<CasesFindResponseUI> => {
|
||||
const toasts = useToasts();
|
||||
const { owner } = useCasesContext();
|
||||
const { owner, settings } = useCasesContext();
|
||||
const availableSolutions = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete'));
|
||||
|
||||
const hasOwner = !!owner.length;
|
||||
|
@ -45,6 +47,23 @@ export const useGetCases = (
|
|||
? { owner: params.filterOptions.owner }
|
||||
: { owner: initialOwner };
|
||||
|
||||
// overrides for incremental_id search
|
||||
let overrides: Partial<FilterOptions> = {};
|
||||
if (settings.displayIncrementalCaseId) {
|
||||
let search = params.filterOptions?.search?.trim();
|
||||
const isIncrementalIdSearch = incrementalIdRegEx.test(search ?? '');
|
||||
if (search && isIncrementalIdSearch) {
|
||||
// extract the number portion of the inc id search: #123 -> 123
|
||||
search = incrementalIdRegEx.exec(search)?.[1] ?? search;
|
||||
// search only in `incremental_id` since types with `title`
|
||||
// and `description` don't overlap
|
||||
overrides = {
|
||||
searchFields: ['incremental_id'],
|
||||
search,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return useQuery(
|
||||
casesQueriesKeys.cases(params),
|
||||
({ signal }) => {
|
||||
|
@ -53,6 +72,7 @@ export const useGetCases = (
|
|||
...DEFAULT_FILTER_OPTIONS,
|
||||
...(params.filterOptions ?? {}),
|
||||
...ownerFilter,
|
||||
...overrides,
|
||||
},
|
||||
queryParams: {
|
||||
...DEFAULT_QUERY_PARAMS,
|
||||
|
|
|
@ -28,6 +28,7 @@ function getConfig(overrides = {}) {
|
|||
markdownPlugins: { lens: true },
|
||||
files: { maxSize: 1, allowedMimeTypes: ALLOWED_MIME_TYPES },
|
||||
stack: { enabled: true },
|
||||
incrementalId: { enabled: true },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { SavedObject } from '@kbn/core-saved-objects-server';
|
||||
import { CaseIdIncrementerAttributesRt } from '../../../common/types/domain/incremental_id/latest';
|
||||
|
||||
export interface CaseIdIncrementerPersistedAttributes {
|
||||
'@timestamp': number;
|
||||
last_id: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export type CaseIdIncrementerTransformedAttributes = CaseIdIncrementerPersistedAttributes;
|
||||
|
||||
export const CaseIdIncrementerTransformedAttributesRt = CaseIdIncrementerAttributesRt;
|
||||
|
||||
export type CaseIdIncrementerSavedObject = SavedObject<CaseIdIncrementerPersistedAttributes>;
|
|
@ -104,6 +104,11 @@ describe('config validation', () => {
|
|||
"application/pdf",
|
||||
],
|
||||
},
|
||||
"incrementalId": Object {
|
||||
"enabled": false,
|
||||
"taskIntervalMinutes": 10,
|
||||
"taskStartDelayMinutes": 10,
|
||||
},
|
||||
"markdownPlugins": Object {
|
||||
"lens": true,
|
||||
},
|
||||
|
|
|
@ -23,6 +23,26 @@ export const ConfigSchema = schema.object({
|
|||
stack: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
incrementalId: schema.object({
|
||||
/**
|
||||
* Whether the incremental id service should be enabled
|
||||
*/
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
/**
|
||||
* The interval that the task should be scheduled at
|
||||
*/
|
||||
taskIntervalMinutes: schema.number({
|
||||
defaultValue: 10,
|
||||
min: 5,
|
||||
}),
|
||||
/**
|
||||
* The initial delay the task will be started with
|
||||
*/
|
||||
taskStartDelayMinutes: schema.number({
|
||||
defaultValue: 10,
|
||||
min: 1,
|
||||
}),
|
||||
}),
|
||||
analytics: schema.object({
|
||||
index: schema.maybe(
|
||||
schema.object({
|
||||
|
|
|
@ -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 const ATTACK_DISCOVERY_MAX_OPEN_CASES = 20;
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { groupAttackDiscoveryAlerts } from '.';
|
||||
import { attackDiscoveryAlerts } from './index.mock';
|
||||
import { groupAttackDiscoveryAlerts } from './group_alerts';
|
||||
import { attackDiscoveryAlerts } from './group_alerts.mock';
|
||||
|
||||
describe('groupAttackDiscoveryAlerts', () => {
|
||||
const getAttackDiscoveryDocument = () => attackDiscoveryAlerts[0];
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getAttackDiscoveryMarkdown } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { MAX_DOCS_PER_PAGE, MAX_TITLE_LENGTH } from '../../../../common/constants';
|
||||
import { AttackDiscoveryExpandedAlertsSchema } from './schema';
|
||||
import type { CaseAlert, CasesGroupedAlerts } from '../types';
|
||||
import { MAX_OPEN_CASES } from '../constants';
|
||||
|
||||
export const groupAttackDiscoveryAlerts = (alerts: CaseAlert[]): CasesGroupedAlerts[] => {
|
||||
/**
|
||||
* First we should validate that the alerts array schema complies with the attack discovery object.
|
||||
*/
|
||||
const attackDiscoveryAlerts = AttackDiscoveryExpandedAlertsSchema.validate(
|
||||
alerts,
|
||||
{},
|
||||
undefined,
|
||||
{ stripUnknownKeys: true }
|
||||
);
|
||||
|
||||
if (attackDiscoveryAlerts.length > MAX_OPEN_CASES) {
|
||||
throw new Error(
|
||||
`Circuit breaker: Attack discovery alerts grouping would create more than the maximum number of allowed cases ${MAX_OPEN_CASES}.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each attack discovery alert we would like to create one separate case.
|
||||
*/
|
||||
const groupedAlerts = attackDiscoveryAlerts.map((attackAlert) => {
|
||||
const alertsIndexPattern = attackAlert.kibana.alert.rule.parameters.alertsIndexPattern;
|
||||
const attackDiscoveryId = attackAlert._id;
|
||||
const attackDiscovery = attackAlert.kibana.alert.attack_discovery;
|
||||
const alertIds = attackDiscovery.alert_ids;
|
||||
|
||||
const caseTitle = attackDiscovery.title.slice(0, MAX_TITLE_LENGTH);
|
||||
const caseComments = [
|
||||
getAttackDiscoveryMarkdown({
|
||||
attackDiscovery: {
|
||||
id: attackDiscoveryId,
|
||||
alertIds,
|
||||
detailsMarkdown: attackDiscovery.details_markdown,
|
||||
entitySummaryMarkdown: attackDiscovery.entity_summary_markdown,
|
||||
mitreAttackTactics: attackDiscovery.mitre_attack_tactics,
|
||||
summaryMarkdown: attackDiscovery.summary_markdown,
|
||||
title: caseTitle,
|
||||
},
|
||||
replacements: attackDiscovery.replacements?.reduce((acc: Record<string, string>, r) => {
|
||||
acc[r.uuid] = r.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
}),
|
||||
].slice(0, MAX_DOCS_PER_PAGE / 2);
|
||||
|
||||
/**
|
||||
* Each attack discovery alert references a list of SIEM alerts that led to the attack.
|
||||
* These SIEM alerts will be added to the case.
|
||||
*/
|
||||
return {
|
||||
alerts: alertIds.map((siemAlertId) => ({ _id: siemAlertId, _index: alertsIndexPattern })),
|
||||
grouping: { attack_discovery: attackDiscoveryId },
|
||||
comments: caseComments,
|
||||
title: caseTitle,
|
||||
};
|
||||
});
|
||||
|
||||
return groupedAlerts;
|
||||
};
|
|
@ -5,69 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getAttackDiscoveryMarkdown } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { MAX_DOCS_PER_PAGE, MAX_TITLE_LENGTH } from '../../../../common/constants';
|
||||
import { AttackDiscoveryExpandedAlertsSchema } from './schema';
|
||||
import type { CaseAlert, CasesGroupedAlerts } from '../types';
|
||||
import { MAX_OPEN_CASES } from '../constants';
|
||||
|
||||
export const groupAttackDiscoveryAlerts = (alerts: CaseAlert[]): CasesGroupedAlerts[] => {
|
||||
/**
|
||||
* First we should validate that the alerts array schema complies with the attack discovery object.
|
||||
*/
|
||||
const attackDiscoveryAlerts = AttackDiscoveryExpandedAlertsSchema.validate(
|
||||
alerts,
|
||||
{},
|
||||
undefined,
|
||||
{ stripUnknownKeys: true }
|
||||
);
|
||||
|
||||
if (attackDiscoveryAlerts.length > MAX_OPEN_CASES) {
|
||||
throw new Error(
|
||||
`Circuit breaker: Attack discovery alerts grouping would create more than the maximum number of allowed cases ${MAX_OPEN_CASES}.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each attack discovery alert we would like to create one separate case.
|
||||
*/
|
||||
const groupedAlerts = attackDiscoveryAlerts.map((attackAlert) => {
|
||||
const alertsIndexPattern = attackAlert.kibana.alert.rule.parameters.alertsIndexPattern;
|
||||
const attackDiscoveryId = attackAlert._id;
|
||||
const attackDiscovery = attackAlert.kibana.alert.attack_discovery;
|
||||
const alertIds = attackDiscovery.alert_ids;
|
||||
|
||||
const caseTitle = attackDiscovery.title.slice(0, MAX_TITLE_LENGTH);
|
||||
const caseComments = [
|
||||
getAttackDiscoveryMarkdown({
|
||||
attackDiscovery: {
|
||||
id: attackDiscoveryId,
|
||||
alertIds,
|
||||
detailsMarkdown: attackDiscovery.details_markdown,
|
||||
entitySummaryMarkdown: attackDiscovery.entity_summary_markdown,
|
||||
mitreAttackTactics: attackDiscovery.mitre_attack_tactics,
|
||||
summaryMarkdown: attackDiscovery.summary_markdown,
|
||||
title: caseTitle,
|
||||
},
|
||||
replacements: attackDiscovery.replacements?.reduce((acc: Record<string, string>, r) => {
|
||||
acc[r.uuid] = r.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
}),
|
||||
].slice(0, MAX_DOCS_PER_PAGE / 2);
|
||||
|
||||
/**
|
||||
* Each attack discovery alert references a list of SIEM alerts that led to the attack.
|
||||
* These SIEM alerts will be added to the case.
|
||||
*/
|
||||
return {
|
||||
alerts: alertIds.map((siemAlertId) => ({ _id: siemAlertId, _index: alertsIndexPattern })),
|
||||
grouping: { attack_discovery: attackDiscoveryId },
|
||||
comments: caseComments,
|
||||
title: caseTitle,
|
||||
};
|
||||
});
|
||||
|
||||
return groupedAlerts;
|
||||
};
|
||||
export * from './constants';
|
||||
export * from './group_alerts';
|
||||
export * from './types';
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import type { AttackDiscoveryExpandedAlertsSchema } from './schema';
|
||||
import type {
|
||||
AttackDiscoveryExpandedAlertSchema,
|
||||
AttackDiscoveryExpandedAlertsSchema,
|
||||
} from './schema';
|
||||
|
||||
export type AttackDiscoveryExpandedAlert = TypeOf<typeof AttackDiscoveryExpandedAlertSchema>;
|
||||
export type AttackDiscoveryExpandedAlerts = TypeOf<typeof AttackDiscoveryExpandedAlertsSchema>;
|
||||
|
|
|
@ -2768,7 +2768,7 @@ describe('CasesConnectorExecutor', () => {
|
|||
alerts: allAlerts,
|
||||
groupingBy: ['host.name'],
|
||||
// MAX_OPEN_CASES < maximumCasesToOpen
|
||||
maximumCasesToOpen: 20,
|
||||
maximumCasesToOpen: 30,
|
||||
});
|
||||
|
||||
expect(mockGetRecordId).toHaveBeenCalledTimes(1);
|
||||
|
@ -2786,7 +2786,7 @@ describe('CasesConnectorExecutor', () => {
|
|||
alerts: allAlerts,
|
||||
groupingBy: ['host.name'],
|
||||
// MAX_OPEN_CASES < maximumCasesToOpen
|
||||
maximumCasesToOpen: 20,
|
||||
maximumCasesToOpen: 30,
|
||||
});
|
||||
|
||||
expect(mockGetCaseId).toHaveBeenCalledTimes(1);
|
||||
|
@ -2805,7 +2805,7 @@ describe('CasesConnectorExecutor', () => {
|
|||
alerts: allAlerts,
|
||||
groupingBy: ['host.name'],
|
||||
// MAX_OPEN_CASES < maximumCasesToOpen
|
||||
maximumCasesToOpen: 20,
|
||||
maximumCasesToOpen: 30,
|
||||
});
|
||||
|
||||
expect(casesClientMock.attachments.bulkCreate).toHaveBeenCalledTimes(1);
|
||||
|
@ -2832,11 +2832,11 @@ describe('CasesConnectorExecutor', () => {
|
|||
alerts: allAlerts,
|
||||
groupingBy: ['host.name'],
|
||||
// MAX_OPEN_CASES < maximumCasesToOpen
|
||||
maximumCasesToOpen: 20,
|
||||
maximumCasesToOpen: 30,
|
||||
});
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
`[CasesConnector][CasesConnectorExecutor][applyCircuitBreakers] Circuit breaker: Grouping definition would create more than the maximum number of allowed cases 10. Falling back to one case.`,
|
||||
`[CasesConnector][CasesConnectorExecutor][applyCircuitBreakers] Circuit breaker: Grouping definition would create more than the maximum number of allowed cases 20. Falling back to one case.`,
|
||||
{ labels: {}, tags: ['cases-connector', 'rule:rule-test-id'] }
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { CustomFieldTypes } from '../../../common/types/domain';
|
||||
|
||||
export const MAX_CONCURRENT_ES_REQUEST = 5;
|
||||
export const MAX_OPEN_CASES = 10;
|
||||
export const MAX_OPEN_CASES = 20;
|
||||
export const DEFAULT_MAX_OPEN_CASES = 5;
|
||||
export const INITIAL_ORACLE_RECORD_COUNTER = 1;
|
||||
|
||||
|
|
|
@ -12,7 +12,10 @@ import { AlertConsumers } from '@kbn/rule-data-utils';
|
|||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common';
|
||||
import { attackDiscoveryAlerts } from './attack_discovery/index.mock';
|
||||
import { attackDiscoveryAlerts } from './attack_discovery/group_alerts.mock';
|
||||
import { DEFAULT_MAX_OPEN_CASES } from './constants';
|
||||
import type { AttackDiscoveryExpandedAlert } from './attack_discovery';
|
||||
import { ATTACK_DISCOVERY_MAX_OPEN_CASES } from './attack_discovery';
|
||||
|
||||
describe('getCasesConnectorType', () => {
|
||||
const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
@ -424,6 +427,36 @@ describe('getCasesConnectorType', () => {
|
|||
recovered: { data: [], count: 0 },
|
||||
};
|
||||
|
||||
it('returns `internallyManagedAlerts` set to `true`', () => {
|
||||
const adapter = getCasesConnectorAdapter({ logger: mockLogger });
|
||||
|
||||
const connectorParams = adapter.buildActionParams({
|
||||
// @ts-expect-error: not all fields are needed
|
||||
alerts: alertsMock,
|
||||
rule: attackDiscoveryRule,
|
||||
params: getParams(),
|
||||
spaceId: 'default',
|
||||
});
|
||||
|
||||
expect(connectorParams.subActionParams.internallyManagedAlerts).toBe(true);
|
||||
});
|
||||
|
||||
it('returns `maximumCasesToOpen` set to `ATTACK_DISCOVERY_MAX_OPEN_CASES`', () => {
|
||||
const adapter = getCasesConnectorAdapter({ logger: mockLogger });
|
||||
|
||||
const connectorParams = adapter.buildActionParams({
|
||||
// @ts-expect-error: not all fields are needed
|
||||
alerts: alertsMock,
|
||||
rule: attackDiscoveryRule,
|
||||
params: getParams(),
|
||||
spaceId: 'default',
|
||||
});
|
||||
|
||||
expect(connectorParams.subActionParams.maximumCasesToOpen).toBe(
|
||||
ATTACK_DISCOVERY_MAX_OPEN_CASES
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly groups attack discovery alerts', () => {
|
||||
const adapter = getCasesConnectorAdapter({ logger: mockLogger });
|
||||
|
||||
|
@ -494,6 +527,35 @@ describe('getCasesConnectorType', () => {
|
|||
expect(connectorParams.subActionParams.internallyManagedAlerts).toBe(true);
|
||||
});
|
||||
|
||||
it('correctly fallsback to general flow if alerts count is above the limit', () => {
|
||||
const adapter = getCasesConnectorAdapter({ logger: mockLogger });
|
||||
|
||||
const manyAttackDiscoveryAlerts = new Array<AttackDiscoveryExpandedAlert>(
|
||||
ATTACK_DISCOVERY_MAX_OPEN_CASES + 1
|
||||
).fill(attackDiscoveryAlerts[0]);
|
||||
const manyAlerts = {
|
||||
all: { data: [...manyAttackDiscoveryAlerts], count: manyAttackDiscoveryAlerts.length },
|
||||
new: { data: [...manyAttackDiscoveryAlerts], count: manyAttackDiscoveryAlerts.length },
|
||||
ongoing: { data: [], count: 0 },
|
||||
recovered: { data: [], count: 0 },
|
||||
};
|
||||
|
||||
const connectorParams = adapter.buildActionParams({
|
||||
// @ts-expect-error: not all fields are needed
|
||||
alerts: manyAlerts,
|
||||
rule: attackDiscoveryRule,
|
||||
params: getParams(),
|
||||
spaceId: 'default',
|
||||
});
|
||||
|
||||
expect(connectorParams.subActionParams.groupedAlerts).toBeNull();
|
||||
expect(connectorParams.subActionParams.internallyManagedAlerts).toBe(false);
|
||||
expect(connectorParams.subActionParams.maximumCasesToOpen).toBe(DEFAULT_MAX_OPEN_CASES);
|
||||
expect(mockLogger.error).toBeCalledWith(
|
||||
'Could not setup grouped Attack Discovery alerts, because of error: Error: Circuit breaker: Attack discovery alerts grouping would create more than the maximum number of allowed cases 20.'
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly fallsback to general flow if alerts schema does not pass validation', () => {
|
||||
const adapter = getCasesConnectorAdapter({ logger: mockLogger });
|
||||
|
||||
|
@ -507,6 +569,7 @@ describe('getCasesConnectorType', () => {
|
|||
|
||||
expect(connectorParams.subActionParams.groupedAlerts).toBeNull();
|
||||
expect(connectorParams.subActionParams.internallyManagedAlerts).toBe(false);
|
||||
expect(connectorParams.subActionParams.maximumCasesToOpen).toBe(DEFAULT_MAX_OPEN_CASES);
|
||||
expect(mockLogger.error).toBeCalledWith(
|
||||
'Could not setup grouped Attack Discovery alerts, because of error: Error: [0.kibana.alert.attack_discovery.alert_ids]: expected value of type [array] but got [undefined]'
|
||||
);
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
} from './schema';
|
||||
import type { CasesClient } from '../../client';
|
||||
import { constructRequiredKibanaPrivileges } from './utils';
|
||||
import { groupAttackDiscoveryAlerts } from './attack_discovery';
|
||||
import { ATTACK_DISCOVERY_MAX_OPEN_CASES, groupAttackDiscoveryAlerts } from './attack_discovery';
|
||||
|
||||
interface GetCasesConnectorTypeArgs {
|
||||
getCasesClient: (request: KibanaRequest) => Promise<CasesClient>;
|
||||
|
@ -109,10 +109,12 @@ export const getCasesConnectorAdapter = ({
|
|||
*/
|
||||
let internallyManagedAlerts = false;
|
||||
let groupedAlerts: CasesGroupedAlerts[] | null = null;
|
||||
let maximumCasesToOpen = DEFAULT_MAX_OPEN_CASES;
|
||||
if (rule.ruleTypeId === ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID) {
|
||||
try {
|
||||
groupedAlerts = groupAttackDiscoveryAlerts(caseAlerts);
|
||||
internallyManagedAlerts = true;
|
||||
maximumCasesToOpen = ATTACK_DISCOVERY_MAX_OPEN_CASES;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Could not setup grouped Attack Discovery alerts, because of error: ${error}`
|
||||
|
@ -134,7 +136,7 @@ export const getCasesConnectorAdapter = ({
|
|||
owner,
|
||||
reopenClosedCases: params.subActionParams.reopenClosedCases,
|
||||
timeWindow: params.subActionParams.timeWindow,
|
||||
maximumCasesToOpen: DEFAULT_MAX_OPEN_CASES,
|
||||
maximumCasesToOpen,
|
||||
templateId: params.subActionParams.templateId,
|
||||
internallyManagedAlerts,
|
||||
};
|
||||
|
|
|
@ -193,13 +193,13 @@ describe('CasesConnectorRunParamsSchema', () => {
|
|||
).toThrow();
|
||||
});
|
||||
|
||||
it('does not accept maximumCasesToOpen to be more than 10', () => {
|
||||
it('does not accept maximumCasesToOpen to be more than 20', () => {
|
||||
const params = getParams();
|
||||
|
||||
expect(() =>
|
||||
CasesConnectorRunParamsSchema.validate({
|
||||
...params,
|
||||
maximumCasesToOpen: 11,
|
||||
maximumCasesToOpen: 21,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue