diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index fbbfc25e89a26..60b702fa1d8fc 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -34,7 +34,9 @@ disabled: # Cypress configs, for now these are still run manually - x-pack/test/fleet_cypress/cli_config.ts + - x-pack/test/fleet_cypress/cli_config.space_awareness.ts - x-pack/test/fleet_cypress/config.ts + - x-pack/test/fleet_cypress/config.space_awareness.ts - x-pack/test/fleet_cypress/visual_config.ts defaultQueue: 'n2-4-spot' diff --git a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml index 2bd85e2ad8a74..3e0e75cd70580 100644 --- a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml +++ b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml @@ -44,3 +44,4 @@ steps: command: "make -C /agent trigger-manual-verification-phase" agents: image: "docker.elastic.co/ci-agent-images/manual-verification-agent:0.0.6" + if: build.env("DEPLOYMENT_SLICES") =~ /.*staging-ds-2.*/ diff --git a/.buildkite/scripts/steps/functional/fleet_cypress.sh b/.buildkite/scripts/steps/functional/fleet_cypress.sh index 43eb329f860b7..e050b73a91c3e 100755 --- a/.buildkite/scripts/steps/functional/fleet_cypress.sh +++ b/.buildkite/scripts/steps/functional/fleet_cypress.sh @@ -12,4 +12,4 @@ echo "--- Fleet Cypress tests (Chrome)" cd x-pack/plugins/fleet set +e -yarn cypress:run:reporter; status=$?; yarn junit:merge || :; exit $status +yarn cypress:run:reporter; status=$?; yarn cypress_space_awareness:run:reporter; space_status=$?; yarn junit:merge || :; [ "$status" -ne 0 ] && exit $status || [ "$space_status" -ne 0 ] && exit $space_status || exit 0 diff --git a/config/serverless.security.yml b/config/serverless.security.yml index 0396857d7ed38..ea83f23a2311b 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -118,3 +118,8 @@ xpack.ml.compatibleModuleType: 'security' # Disable the embedded Dev Console console.ui.embeddedEnabled: false + +# Experimental Security Solution features + +# This feature is disabled in Serverless until fully performance tested within a Serverless environment +xpack.securitySolution.enableExperimental: ['entityStoreDisabled'] diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 636b328b3a9c3..070df6ce68bbb 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -8394,6 +8394,56 @@ paths: summary: Stop an Entity Engine tags: - Security Entity Analytics API + /api/entity_store/engines/apply_dataview_indices: + post: + operationId: ApplyEntityEngineDataviewIndices + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Successful response + '207': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + errors: + items: + type: string + type: array + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Partial successful response + '500': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + body: + type: string + statusCode: + type: number + description: Error response + summary: Apply DataView indices to all installed engines + tags: + - Security Entity Analytics API /api/entity_store/entities/list: get: description: 'List entities records, paging, sorting and filtering as needed.' @@ -30170,6 +30220,20 @@ components: #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel required: - criticality_level + Security_Entity_Analytics_API_EngineDataviewUpdateResult: + type: object + properties: + changes: + type: object + properties: + indexPatterns: + items: + type: string + type: array + type: + type: string + required: + - type Security_Entity_Analytics_API_EngineDescriptor: type: object properties: @@ -30193,6 +30257,7 @@ components: - installing - started - stopped + - updating type: string Security_Entity_Analytics_API_Entity: oneOf: diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 636b328b3a9c3..070df6ce68bbb 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -8394,6 +8394,56 @@ paths: summary: Stop an Entity Engine tags: - Security Entity Analytics API + /api/entity_store/engines/apply_dataview_indices: + post: + operationId: ApplyEntityEngineDataviewIndices + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Successful response + '207': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + errors: + items: + type: string + type: array + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Partial successful response + '500': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + body: + type: string + statusCode: + type: number + description: Error response + summary: Apply DataView indices to all installed engines + tags: + - Security Entity Analytics API /api/entity_store/entities/list: get: description: 'List entities records, paging, sorting and filtering as needed.' @@ -30170,6 +30220,20 @@ components: #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel required: - criticality_level + Security_Entity_Analytics_API_EngineDataviewUpdateResult: + type: object + properties: + changes: + type: object + properties: + indexPatterns: + items: + type: string + type: array + type: + type: string + required: + - type Security_Entity_Analytics_API_EngineDescriptor: type: object properties: @@ -30193,6 +30257,7 @@ components: - installing - started - stopped + - updating type: string Security_Entity_Analytics_API_Entity: oneOf: diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 69749c5e11d64..f6b2055563211 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -11822,6 +11822,56 @@ paths: summary: Stop an Entity Engine tags: - Security Entity Analytics API + /api/entity_store/engines/apply_dataview_indices: + post: + operationId: ApplyEntityEngineDataviewIndices + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Successful response + '207': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + errors: + items: + type: string + type: array + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Partial successful response + '500': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + body: + type: string + statusCode: + type: number + description: Error response + summary: Apply DataView indices to all installed engines + tags: + - Security Entity Analytics API /api/entity_store/entities/list: get: description: 'List entities records, paging, sorting and filtering as needed.' @@ -38935,6 +38985,20 @@ components: #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel required: - criticality_level + Security_Entity_Analytics_API_EngineDataviewUpdateResult: + type: object + properties: + changes: + type: object + properties: + indexPatterns: + items: + type: string + type: array + type: + type: string + required: + - type Security_Entity_Analytics_API_EngineDescriptor: type: object properties: @@ -38958,6 +39022,7 @@ components: - installing - started - stopped + - updating type: string Security_Entity_Analytics_API_Entity: oneOf: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 69749c5e11d64..f6b2055563211 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -11822,6 +11822,56 @@ paths: summary: Stop an Entity Engine tags: - Security Entity Analytics API + /api/entity_store/engines/apply_dataview_indices: + post: + operationId: ApplyEntityEngineDataviewIndices + responses: + '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Successful response + '207': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + errors: + items: + type: string + type: array + result: + items: + $ref: >- + #/components/schemas/Security_Entity_Analytics_API_EngineDataviewUpdateResult + type: array + success: + type: boolean + description: Partial successful response + '500': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + type: object + properties: + body: + type: string + statusCode: + type: number + description: Error response + summary: Apply DataView indices to all installed engines + tags: + - Security Entity Analytics API /api/entity_store/entities/list: get: description: 'List entities records, paging, sorting and filtering as needed.' @@ -38935,6 +38985,20 @@ components: #/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel required: - criticality_level + Security_Entity_Analytics_API_EngineDataviewUpdateResult: + type: object + properties: + changes: + type: object + properties: + indexPatterns: + items: + type: string + type: array + type: + type: string + required: + - type Security_Entity_Analytics_API_EngineDescriptor: type: object properties: @@ -38958,6 +39022,7 @@ components: - installing - started - stopped + - updating type: string Security_Entity_Analytics_API_Entity: oneOf: diff --git a/package.json b/package.json index 788d5cf53b481..e9f675a0a46d3 100644 --- a/package.json +++ b/package.json @@ -218,8 +218,8 @@ "@kbn/cloud-integration-saml-provider-plugin": "link:x-pack/test/cloud_integration/plugins/saml_provider", "@kbn/cloud-links-plugin": "link:x-pack/plugins/cloud_integrations/cloud_links", "@kbn/cloud-plugin": "link:x-pack/plugins/cloud", - "@kbn/cloud-security-posture": "link:x-pack/packages/kbn-cloud-security-posture", - "@kbn/cloud-security-posture-common": "link:x-pack/packages/kbn-cloud-security-posture-common", + "@kbn/cloud-security-posture": "link:x-pack/packages/kbn-cloud-security-posture/public", + "@kbn/cloud-security-posture-common": "link:x-pack/packages/kbn-cloud-security-posture/common", "@kbn/cloud-security-posture-graph": "link:x-pack/packages/kbn-cloud-security-posture/graph", "@kbn/cloud-security-posture-plugin": "link:x-pack/plugins/cloud_security_posture", "@kbn/code-editor": "link:packages/shared-ux/code_editor/impl", @@ -1587,7 +1587,7 @@ "@types/jsonwebtoken": "^9.0.0", "@types/license-checker": "15.0.0", "@types/loader-utils": "^2.0.3", - "@types/lodash": "^4.14.159", + "@types/lodash": "^4.17.10", "@types/lru-cache": "^5.1.0", "@types/lz-string": "^1.3.34", "@types/mapbox__vector-tile": "1.3.0", diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts index 8f4e59d80458b..9a719c24076f7 100644 --- a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts +++ b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts @@ -54,6 +54,6 @@ export const transformUpdateRuleBody: RewriteResponseCase = ({ ...(uuid && { uuid }), }; }), - ...(alertDelay ? { alert_delay: alertDelay } : {}), + ...(alertDelay !== undefined ? { alert_delay: alertDelay } : {}), ...(flapping !== undefined ? { flapping: transformUpdateRuleFlapping(flapping) } : {}), }); diff --git a/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts index 49ea5a63b3fca..542bb055fd431 100644 --- a/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts +++ b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts @@ -8,4 +8,4 @@ */ // Feature flag for frontend rule specific flapping in rule flyout -export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false; +export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = true; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts index 4ee00a94b90ed..ebdfeeafbe2fd 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts @@ -10,10 +10,11 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpStart, IHttpFetchError } from '@kbn/core-http-browser'; import { createRule, CreateRuleBody } from '../apis/create_rule'; +import { Rule } from '../types'; export interface UseCreateRuleProps { http: HttpStart; - onSuccess?: (formData: CreateRuleBody) => void; + onSuccess?: (rule: Rule) => void; onError?: (error: IHttpFetchError<{ message: string }>) => void; } diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts index 9ae876d06278b..8c93881762b1a 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts @@ -15,10 +15,11 @@ export interface UseLoadConnectorsProps { http: HttpStart; includeSystemActions?: boolean; enabled?: boolean; + cacheTime?: number; } export const useLoadConnectors = (props: UseLoadConnectorsProps) => { - const { http, includeSystemActions = false, enabled = true } = props; + const { http, includeSystemActions = false, enabled = true, cacheTime } = props; const queryFn = () => { return fetchConnectors({ http, includeSystemActions }); @@ -27,6 +28,7 @@ export const useLoadConnectors = (props: UseLoadConnectorsProps) => { const { data, isLoading, isFetching, isInitialLoading } = useQuery({ queryKey: ['useLoadConnectors', includeSystemActions], queryFn, + cacheTime, refetchOnWindowFocus: false, enabled, }); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts index fab6fd3336f2e..c9dbc6c75ff35 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts @@ -17,10 +17,11 @@ export interface UseLoadRuleTypeAadTemplateFieldProps { http: HttpStart; ruleTypeId?: string; enabled: boolean; + cacheTime?: number; } export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplateFieldProps) => { - const { http, ruleTypeId, enabled } = props; + const { http, ruleTypeId, enabled, cacheTime } = props; const queryFn = () => { if (!ruleTypeId) { @@ -43,6 +44,7 @@ export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplat description: getDescription(d.name, EcsFlat), })); }, + cacheTime, refetchOnWindowFocus: false, enabled, }); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts index fafd372dc3640..95c3ca6baad02 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts @@ -15,10 +15,11 @@ import { RuleFormData } from '../../rule_form'; export interface UseResolveProps { http: HttpStart; id?: string; + cacheTime?: number; } export const useResolveRule = (props: UseResolveProps) => { - const { id, http } = props; + const { id, http, cacheTime } = props; const queryFn = () => { if (id) { @@ -30,6 +31,7 @@ export const useResolveRule = (props: UseResolveProps) => { queryKey: ['useResolveRule', id], queryFn, enabled: !!id, + cacheTime, select: (rule): RuleFormData | null => { if (!rule) { return null; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts index 0e8199fc1cca2..5764b8128ef42 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts @@ -10,10 +10,11 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpStart, IHttpFetchError } from '@kbn/core-http-browser'; import { updateRule, UpdateRuleBody } from '../apis/update_rule'; +import { Rule } from '../types'; export interface UseUpdateRuleProps { http: HttpStart; - onSuccess?: (formData: UpdateRuleBody) => void; + onSuccess?: (rule: Rule) => void; onError?: (error: IHttpFetchError<{ message: string }>) => void; } diff --git a/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts b/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts index 40498f1a27886..29eaf17552a2b 100644 --- a/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts +++ b/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts @@ -27,8 +27,6 @@ import { TypeRegistry } from '../type_registry'; export type { SanitizedRuleAction as RuleAction } from '@kbn/alerting-types'; -export type { Flapping } from '@kbn/alerting-types'; - export type RuleTypeWithDescription = RuleType & { description?: string }; export type RuleTypeIndexWithDescriptions = Map; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts b/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts index f557dc5ebdb42..a3748eeabe697 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts @@ -27,7 +27,7 @@ export const DEFAULT_FREQUENCY = { summary: false, }; -export const GET_DEFAULT_FORM_DATA = ({ +export const getDefaultFormData = ({ ruleTypeId, name, consumer, @@ -50,6 +50,7 @@ export const GET_DEFAULT_FORM_DATA = ({ ruleTypeId, name, actions, + alertDelay: { active: 1 }, }; }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx index fc96ae214a7a8..4399dc5239ec7 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx @@ -12,7 +12,7 @@ import { EuiLoadingElastic } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import type { RuleFormData, RuleFormPlugins } from './types'; -import { DEFAULT_VALID_CONSUMERS, GET_DEFAULT_FORM_DATA } from './constants'; +import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; import { RuleFormStateProvider } from './rule_form_state'; import { useCreateRule } from '../common/hooks'; import { RulePage } from './rule_page'; @@ -24,6 +24,7 @@ import { } from './rule_form_errors'; import { useLoadDependencies } from './hooks/use_load_dependencies'; import { + getAvailableRuleTypes, getInitialConsumer, getInitialMultiConsumer, getInitialSchedule, @@ -42,7 +43,8 @@ export interface CreateRuleFormProps { shouldUseRuleProducer?: boolean; canShowConsumerSelection?: boolean; showMustacheAutocompleteSwitch?: boolean; - returnUrl: string; + onCancel?: () => void; + onSubmit?: (ruleId: string) => void; } export const CreateRuleForm = (props: CreateRuleFormProps) => { @@ -56,7 +58,8 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { shouldUseRuleProducer = false, canShowConsumerSelection = true, showMustacheAutocompleteSwitch = false, - returnUrl, + onCancel, + onSubmit, } = props; const { http, docLinks, notifications, ruleTypeRegistry, i18n, theme } = plugins; @@ -64,8 +67,9 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { const { mutate, isLoading: isSaving } = useCreateRule({ http, - onSuccess: ({ name }) => { + onSuccess: ({ name, id }) => { toasts.addSuccess(RULE_CREATE_SUCCESS_TEXT(name)); + onSubmit?.(id); }, onError: (error) => { const message = parseRuleCircuitBreakerErrorMessage( @@ -86,6 +90,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { const { isInitialLoading, ruleType, + ruleTypes, ruleTypeModel, uiConfig, healthCheckError, @@ -153,7 +158,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
{ minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleTypeModel: ruleTypeModel, selectedRuleType: ruleType, + availableRuleTypes: getAvailableRuleTypes({ + consumer, + ruleTypes, + ruleTypeRegistry, + }).map(({ ruleType: rt }) => rt), validConsumers, flappingSettings, canShowConsumerSelection, @@ -185,7 +195,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), }} > - +
); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx index 6e92b94cc2e0d..917fc87420f9a 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx @@ -24,17 +24,19 @@ import { RuleFormRuleTypeError, } from './rule_form_errors'; import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations'; -import { parseRuleCircuitBreakerErrorMessage } from './utils'; +import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils'; +import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; export interface EditRuleFormProps { id: string; plugins: RuleFormPlugins; showMustacheAutocompleteSwitch?: boolean; - returnUrl: string; + onCancel?: () => void; + onSubmit?: (ruleId: string) => void; } export const EditRuleForm = (props: EditRuleFormProps) => { - const { id, plugins, returnUrl, showMustacheAutocompleteSwitch = false } = props; + const { id, plugins, showMustacheAutocompleteSwitch = false, onCancel, onSubmit } = props; const { http, notifications, docLinks, ruleTypeRegistry, i18n, theme, application } = plugins; const { toasts } = notifications; @@ -42,6 +44,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { http, onSuccess: ({ name }) => { toasts.addSuccess(RULE_EDIT_SUCCESS_TEXT(name)); + onSubmit?.(id); }, onError: (error) => { const message = parseRuleCircuitBreakerErrorMessage( @@ -62,6 +65,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { const { isInitialLoading, ruleType, + ruleTypes, ruleTypeModel, uiConfig, healthCheckError, @@ -156,17 +160,31 @@ export const EditRuleForm = (props: EditRuleFormProps) => { connectors, connectorTypes, aadTemplateFields, - formData: fetchedFormData, + formData: { + ...getDefaultFormData({ + ruleTypeId: fetchedFormData.ruleTypeId, + name: fetchedFormData.name, + consumer: fetchedFormData.consumer, + actions: fetchedFormData.actions, + }), + ...fetchedFormData, + }, id, plugins, minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleType: ruleType, selectedRuleTypeModel: ruleTypeModel, + availableRuleTypes: getAvailableRuleTypes({ + consumer: fetchedFormData.consumer, + ruleTypes, + ruleTypeRegistry, + }).map(({ ruleType: rt }) => rt), flappingSettings, + validConsumers: DEFAULT_VALID_CONSUMERS, showMustacheAutocompleteSwitch, }} > - + ); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx index 9d2ce3b6f1211..f0a14ac82e4a6 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx @@ -46,10 +46,6 @@ jest.mock('../../common/hooks/use_load_rule_type_aad_template_fields', () => ({ useLoadRuleTypeAadTemplateField: jest.fn(), })); -jest.mock('../utils/get_authorized_rule_types', () => ({ - getAvailableRuleTypes: jest.fn(), -})); - jest.mock('../../common/hooks/use_fetch_flapping_settings', () => ({ useFetchFlappingSettings: jest.fn(), })); @@ -63,7 +59,6 @@ const { useLoadRuleTypeAadTemplateField } = jest.requireMock( '../../common/hooks/use_load_rule_type_aad_template_fields' ); const { useLoadRuleTypesQuery } = jest.requireMock('../../common/hooks/use_load_rule_types_query'); -const { getAvailableRuleTypes } = jest.requireMock('../utils/get_authorized_rule_types'); const { useFetchFlappingSettings } = jest.requireMock( '../../common/hooks/use_fetch_flapping_settings' ); @@ -168,13 +163,6 @@ useLoadRuleTypesQuery.mockReturnValue({ }, }); -getAvailableRuleTypes.mockReturnValue([ - { - ruleType: indexThresholdRuleType, - ruleTypeModel: indexThresholdRuleTypeModel, - }, -]); - const mockConnector = { id: 'test-connector', name: 'Test', @@ -236,7 +224,7 @@ const toastsMock = jest.fn(); const ruleTypeRegistryMock: RuleTypeRegistryContract = { has: jest.fn(), register: jest.fn(), - get: jest.fn(), + get: jest.fn().mockReturnValue(indexThresholdRuleTypeModel), list: jest.fn(), }; @@ -272,6 +260,7 @@ describe('useLoadDependencies', () => { isLoading: false, isInitialLoading: false, ruleType: indexThresholdRuleType, + ruleTypes: [...ruleTypeIndex.values()], ruleTypeModel: indexThresholdRuleTypeModel, uiConfig: uiConfigMock, healthCheckError: null, @@ -317,39 +306,6 @@ describe('useLoadDependencies', () => { }); }); - test('should call getAvailableRuleTypes with the correct params', async () => { - const { result } = renderHook( - () => { - return useLoadDependencies({ - http: httpMock as unknown as HttpStart, - toasts: toastsMock as unknown as ToastsStart, - ruleTypeRegistry: ruleTypeRegistryMock, - validConsumers: ['stackAlerts', 'logs'], - consumer: 'logs', - capabilities: { - actions: { - show: true, - save: true, - execute: true, - }, - } as unknown as ApplicationStart['capabilities'], - }); - }, - { wrapper } - ); - - await waitFor(() => { - return expect(result.current.isInitialLoading).toEqual(false); - }); - - expect(getAvailableRuleTypes).toBeCalledWith({ - consumer: 'logs', - ruleTypeRegistry: ruleTypeRegistryMock, - ruleTypes: [indexThresholdRuleType], - validConsumers: ['stackAlerts', 'logs'], - }); - }); - test('should call resolve rule with the correct params', async () => { const { result } = renderHook( () => { @@ -377,6 +333,7 @@ describe('useLoadDependencies', () => { expect(useResolveRule).toBeCalledWith({ http: httpMock, id: 'test-rule-id', + cacheTime: 0, }); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts index 5e0c52b1089ba..9fb0f173b9d21 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts @@ -20,7 +20,6 @@ import { useLoadUiConfig, useResolveRule, } from '../../common/hooks'; -import { getAvailableRuleTypes } from '../utils'; import { RuleTypeRegistryContract } from '../../common'; import { useFetchFlappingSettings } from '../../common/hooks/use_fetch_flapping_settings'; import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping'; @@ -43,8 +42,6 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { http, toasts, ruleTypeRegistry, - consumer, - validConsumers, id, ruleTypeId, capabilities, @@ -69,7 +66,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { data: fetchedFormData, isLoading: isLoadingRule, isInitialLoading: isInitialLoadingRule, - } = useResolveRule({ http, id }); + } = useResolveRule({ http, id, cacheTime: 0 }); const { ruleTypesState: { @@ -100,6 +97,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { http, includeSystemActions: true, enabled: canReadConnectors, + cacheTime: 0, }); const computedRuleTypeId = useMemo(() => { @@ -125,28 +123,22 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { http, ruleTypeId: computedRuleTypeId, enabled: !!computedRuleTypeId && canReadConnectors, + cacheTime: 0, }); - const authorizedRuleTypeItems = useMemo(() => { - const computedConsumer = consumer || fetchedFormData?.consumer; - if (!computedConsumer) { - return []; + const ruleType = useMemo(() => { + if (!computedRuleTypeId || !ruleTypeIndex) { + return null; } - return getAvailableRuleTypes({ - consumer: computedConsumer, - ruleTypes: [...ruleTypeIndex.values()], - ruleTypeRegistry, - validConsumers, - }); - }, [consumer, ruleTypeIndex, ruleTypeRegistry, validConsumers, fetchedFormData]); - - const [ruleType, ruleTypeModel] = useMemo(() => { - const item = authorizedRuleTypeItems.find(({ ruleType: rt }) => { - return rt.id === computedRuleTypeId; - }); - - return [item?.ruleType, item?.ruleTypeModel]; - }, [authorizedRuleTypeItems, computedRuleTypeId]); + return ruleTypeIndex.get(computedRuleTypeId); + }, [computedRuleTypeId, ruleTypeIndex]); + + const ruleTypeModel = useMemo(() => { + if (!computedRuleTypeId) { + return null; + } + return ruleTypeRegistry.get(computedRuleTypeId); + }, [computedRuleTypeId, ruleTypeRegistry]); const isLoading = useMemo(() => { // Create Mode @@ -227,6 +219,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoading: !!isInitialLoading, ruleType, ruleTypeModel, + ruleTypes: [...ruleTypeIndex.values()], uiConfig, healthCheckError, fetchedFormData, diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx index 63846fb3628ce..9560d933060f6 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx @@ -117,12 +117,18 @@ describe('ruleActions', () => { getActionTypeModel('1', { id: 'actionType-1', validateParams: mockValidate, + defaultActionParams: { + key: 'value', + }, }) ); actionTypeRegistry.register( getActionTypeModel('2', { id: 'actionType-2', validateParams: mockValidate, + defaultActionParams: { + key: 'value', + }, }) ); @@ -150,6 +156,10 @@ describe('ruleActions', () => { selectedRuleType: { id: 'selectedRuleTypeId', defaultActionGroupId: 'test', + recoveryActionGroup: { + id: 'test-recovery-group-id', + name: 'test-recovery-group', + }, producer: 'stackAlerts', }, connectors: mockConnectors, @@ -222,7 +232,7 @@ describe('ruleActions', () => { frequency: { notifyWhen: 'onActionGroupChange', summary: false, throttle: null }, group: 'test', id: 'connector-1', - params: {}, + params: { key: 'value' }, uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', }, type: 'addAction', diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx index b9eb28025205c..47588b487be6d 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx @@ -18,6 +18,7 @@ import { ActionConnector, RuleAction, RuleFormParamsErrors } from '../../common/ import { DEFAULT_FREQUENCY, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; import { RuleActionsItem } from './rule_actions_item'; import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item'; +import { getDefaultParams } from '../utils'; export const RuleActions = () => { const [isConnectorModalOpen, setIsConnectorModalOpen] = useState(false); @@ -44,7 +45,15 @@ export const RuleActions = () => { async (connector: ActionConnector) => { const { id, actionTypeId } = connector; const uuid = uuidv4(); - const params = {}; + const group = selectedRuleType.defaultActionGroupId; + const actionTypeModel = actionTypeRegistry.get(actionTypeId); + + const params = + getDefaultParams({ + group, + ruleType: selectedRuleType, + actionTypeModel, + }) || {}; dispatch({ type: 'addAction', @@ -53,7 +62,7 @@ export const RuleActions = () => { actionTypeId, uuid, params, - group: selectedRuleType.defaultActionGroupId, + group, frequency: DEFAULT_FREQUENCY, }, }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx index 791c1ce0491f2..a5bbacc74d7a5 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx @@ -68,6 +68,7 @@ export const RuleActionsAlertsFilter = ({ () => onChange(state ? undefined : query), [state, query, onChange] ); + const updateQuery = useCallback( (update: Partial) => { setQuery({ diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx index 9c3dbcf15e364..82496d9578ff0 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx @@ -163,7 +163,10 @@ export const RuleActionsConnectorsModal = (props: RuleActionsConnectorsModalProp const connectorFacetButtons = useMemo(() => { return ( - + { await userEvent.click(screen.getByText('onTimeframeChange')); - expect(mockOnChange).toHaveBeenCalledTimes(1); + expect(mockOnChange).toHaveBeenCalledTimes(2); expect(mockOnChange).toHaveBeenCalledWith({ payload: { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx index b80a79a69cfcf..9bf6cac970b19 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx @@ -40,17 +40,12 @@ import { isEmpty, some } from 'lodash'; import { css } from '@emotion/react'; import { SavedObjectAttribute } from '@kbn/core/types'; import { useRuleFormDispatch, useRuleFormState } from '../hooks'; -import { - ActionConnector, - ActionTypeModel, - RuleFormParamsErrors, - RuleTypeWithDescription, -} from '../../common/types'; +import { ActionConnector, RuleFormParamsErrors } from '../../common/types'; import { getAvailableActionVariables } from '../../action_variables'; import { validateAction, validateParamsForWarnings } from '../validation'; import { RuleActionsSettings } from './rule_actions_settings'; -import { getSelectedActionGroup } from '../utils'; +import { getDefaultParams, getSelectedActionGroup } from '../utils'; import { RuleActionsMessage } from './rule_actions_message'; import { ACTION_ERROR_TOOLTIP, @@ -60,6 +55,7 @@ import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL, } from '../translations'; +import { checkActionFormActionTypeEnabled } from '../utils/check_action_type_enabled'; const SUMMARY_GROUP_TITLE = i18n.translate('alertsUIShared.ruleActionsItem.summaryGroupTitle', { defaultMessage: 'Summary of alerts', @@ -83,22 +79,6 @@ const ACTION_TITLE = (connector: ActionConnector) => }, }); -const getDefaultParams = ({ - group, - ruleType, - actionTypeModel, -}: { - group: string; - actionTypeModel: ActionTypeModel; - ruleType: RuleTypeWithDescription; -}) => { - if (group === ruleType.recoveryActionGroup.id) { - return actionTypeModel.defaultRecoveredActionParams; - } else { - return actionTypeModel.defaultActionParams; - } -}; - export interface RuleActionsItemProps { action: RuleAction; index: number; @@ -178,6 +158,16 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { ? aadTemplateFields : availableActionVariables; + const checkEnabledResult = useMemo(() => { + if (!actionType) { + return null; + } + return checkActionFormActionTypeEnabled( + actionType, + connectors.filter((c) => c.isPreconfigured) + ); + }, [actionType, connectors]); + const onDelete = (id: string) => { dispatch({ type: 'removeAction', payload: { uuid: id } }); }; @@ -381,16 +371,24 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { ...action.alertsFilter, query, }; + + if (!newAlertsFilter.query) { + delete newAlertsFilter.query; + } + + const alertsFilter = isEmpty(newAlertsFilter) ? undefined : newAlertsFilter; + const newAction = { ...action, - alertsFilter: newAlertsFilter, + alertsFilter, }; + dispatch({ type: 'setActionProperty', payload: { uuid: action.uuid!, key: 'alertsFilter', - value: newAlertsFilter, + value: alertsFilter, }, }); validateActionBase(newAction); @@ -400,19 +398,33 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { const onTimeframeChange = useCallback( (timeframe?: AlertsFilterTimeframe) => { + const newAlertsFilter = { + ...action.alertsFilter, + timeframe, + }; + + if (!newAlertsFilter.timeframe) { + delete newAlertsFilter.timeframe; + } + + const alertsFilter = isEmpty(newAlertsFilter) ? undefined : newAlertsFilter; + + const newAction = { + ...action, + alertsFilter, + }; + dispatch({ type: 'setActionProperty', payload: { uuid: action.uuid!, key: 'alertsFilter', - value: { - ...action.alertsFilter, - timeframe, - }, + value: alertsFilter, }, }); + validateActionBase(newAction); }, - [action, dispatch] + [action, dispatch, validateActionBase] ); const onUseAadTemplateFieldsChange = useCallback(() => { @@ -443,9 +455,25 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { }, [action, storedActionParamsForAadToggle, dispatch]); const accordionContent = useMemo(() => { - if (!connector) { + if (!connector || !checkEnabledResult) { return null; } + + if (!checkEnabledResult.isEnabled) { + return ( + + {checkEnabledResult.messageCard} + + ); + } + return ( { templateFields, useDefaultMessage, warning, + checkEnabledResult, onNotifyWhenChange, onActionGroupChange, onAlertsFilterChange, diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx index 7b12160c1dadd..327a0ba12634c 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx @@ -74,17 +74,14 @@ describe('RuleAlertDelay', () => { expect(mockOnChange).not.toHaveBeenCalled(); }); - test('Should call onChange with null if empty string is typed', () => { + test('Should not call onChange if empty string is typed', () => { render(); fireEvent.change(screen.getByTestId('alertDelayInput'), { target: { value: '', }, }); - expect(mockOnChange).toHaveBeenCalledWith({ - type: 'setAlertDelay', - payload: null, - }); + expect(mockOnChange).not.toHaveBeenCalled(); }); test('Should display error when input is invalid', () => { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx index 5b26c38232ab4..a79f1f5efe447 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx @@ -28,16 +28,8 @@ export const RuleAlertDelay = () => { const onAlertDelayChange = useCallback( (e: React.ChangeEvent) => { - if (!e.target.validity.valid) { - return; - } - const value = e.target.value; - if (value === '') { - dispatch({ - type: 'setAlertDelay', - payload: null, - }); - } else if (INTEGER_REGEX.test(value)) { + const value = e.target.value.trim(); + if (INTEGER_REGEX.test(value)) { const parsedValue = parseInt(value, 10); dispatch({ type: 'setAlertDelay', @@ -66,7 +58,7 @@ export const RuleAlertDelay = () => { { active: 5, }, notifyWhen: null, - consumer: 'stackAlerts', + consumer: 'alerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], + availableRuleTypes: [ruleType], }); render(); @@ -164,13 +167,16 @@ describe('Rule Definition', () => { active: 5, }, notifyWhen: null, - consumer: 'stackAlerts', + consumer: 'alerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: { ...ruleModel, documentationUrl: null, }, + availableRuleTypes: [ruleType], + validConsumers: ['logs', 'stackAlerts'], }); render(); @@ -191,6 +197,7 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, @@ -215,9 +222,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelect: true, validConsumers: ['logs'], }); @@ -241,9 +250,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelect: true, validConsumers: ['logs', 'observability'], }); @@ -267,9 +278,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], }); render(); @@ -292,9 +305,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], }); render(); @@ -326,9 +341,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], }); @@ -339,6 +356,48 @@ describe('Rule Definition', () => { expect(screen.getByTestId('ruleSettingsFlappingForm')).toBeInTheDocument(); }); + test('should hide flapping if the user does not have read access', async () => { + useRuleFormState.mockReturnValue({ + plugins: { + charts: {} as ChartsPluginSetup, + data: {} as DataPublicPluginStart, + dataViews: {} as DataViewsPublicPluginStart, + unifiedSearch: {} as UnifiedSearchPublicPluginStart, + docLinks: {} as DocLinksStart, + application: { + capabilities: { + rulesSettings: { + readFlappingSettingsUI: false, + writeFlappingSettingsUI: true, + }, + }, + }, + }, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + ruleTypeId: '.es-query', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render(); + + expect(screen.queryByTestId('ruleDefinitionFlappingFormGroup')).not.toBeInTheDocument(); + }); + test('should allow flapping to be changed', async () => { useRuleFormState.mockReturnValue({ plugins, @@ -353,9 +412,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], }); @@ -389,9 +450,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx index 3b404edc5d029..997e666e8340f 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { Suspense, useMemo, useState, useCallback } from 'react'; +import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react'; import { EuiEmptyPrompt, EuiLoadingSpinner, @@ -47,7 +47,7 @@ import { RuleAlertDelay } from './rule_alert_delay'; import { RuleConsumerSelection } from './rule_consumer_selection'; import { RuleSchedule } from './rule_schedule'; import { useRuleFormState, useRuleFormDispatch } from '../hooks'; -import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; +import { ALERTING_FEATURE_ID, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; import { getAuthorizedConsumers } from '../utils'; import { RuleSettingsFlappingTitleTooltip } from '../../rule_settings/rule_settings_flapping_title_tooltip'; import { RuleSettingsFlappingForm } from '../../rule_settings/rule_settings_flapping_form'; @@ -62,6 +62,7 @@ export const RuleDefinition = () => { metadata, selectedRuleType, selectedRuleTypeModel, + availableRuleTypes, validConsumers, canShowConsumerSelection = false, flappingSettings, @@ -70,29 +71,44 @@ export const RuleDefinition = () => { const { colorMode } = useEuiTheme(); const dispatch = useRuleFormDispatch(); + useEffect(() => { + // Need to do a dry run validating the params because the Missing Monitor Data rule type + // does not properly initialize the params + if (selectedRuleType.id === 'monitoring_alert_missing_monitoring_data') { + dispatch({ type: 'runValidation' }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const { charts, data, dataViews, unifiedSearch, docLinks, application } = plugins; const { capabilities: { rulesSettings }, } = application; - const { writeFlappingSettingsUI } = rulesSettings || {}; + const { readFlappingSettingsUI, writeFlappingSettingsUI } = rulesSettings || {}; - const { params, schedule, notifyWhen, flapping } = formData; + const { params, schedule, notifyWhen, flapping, consumer, ruleTypeId } = formData; const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState(false); const [isFlappingPopoverOpen, setIsFlappingPopoverOpen] = useState(false); const authorizedConsumers = useMemo(() => { - if (!validConsumers?.length) { + if (consumer !== ALERTING_FEATURE_ID) { + return []; + } + const selectedAvailableRuleType = availableRuleTypes.find((ruleType) => { + return ruleType.id === selectedRuleType.id; + }); + if (!selectedAvailableRuleType?.authorizedConsumers) { return []; } return getAuthorizedConsumers({ - ruleType: selectedRuleType, + ruleType: selectedAvailableRuleType, validConsumers, }); - }, [selectedRuleType, validConsumers]); + }, [consumer, selectedRuleType, availableRuleTypes, validConsumers]); const shouldShowConsumerSelect = useMemo(() => { if (!canShowConsumerSelection) { @@ -107,10 +123,8 @@ export const RuleDefinition = () => { ) { return false; } - return ( - selectedRuleTypeModel.id && MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleTypeModel.id) - ); - }, [authorizedConsumers, selectedRuleTypeModel, canShowConsumerSelection]); + return !!(ruleTypeId && MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleTypeId)); + }, [ruleTypeId, authorizedConsumers, canShowConsumerSelection]); const RuleParamsExpressionComponent = selectedRuleTypeModel.ruleParamsExpression ?? null; @@ -305,8 +319,9 @@ export const RuleDefinition = () => { > - {IS_RULE_SPECIFIC_FLAPPING_ENABLED && ( + {IS_RULE_SPECIFIC_FLAPPING_ENABLED && readFlappingSettingsUI && ( {ALERT_FLAPPING_DETECTION_TITLE}} description={ diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx index 26342d99580a6..1768303c55223 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx @@ -80,9 +80,6 @@ export const RuleSchedule = () => { const onIntervalNumberChange = useCallback( (e: React.ChangeEvent) => { - if (!e.target.validity.valid) { - return; - } const value = e.target.value.trim(); if (INTEGER_REGEX.test(value)) { const parsedValue = parseInt(value, 10); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx index d1a0f6a56fe2b..c09add5ae1c06 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx @@ -23,11 +23,12 @@ const queryClient = new QueryClient(); export interface RuleFormProps { plugins: RuleFormPlugins; - returnUrl: string; + onCancel?: () => void; + onSubmit?: (ruleId: string) => void; } export const RuleForm = (props: RuleFormProps) => { - const { plugins, returnUrl } = props; + const { plugins, onCancel, onSubmit } = props; const { id, ruleTypeId } = useParams<{ id?: string; ruleTypeId?: string; @@ -35,23 +36,31 @@ export const RuleForm = (props: RuleFormProps) => { const ruleFormComponent = useMemo(() => { if (id) { - return ; + return ; } if (ruleTypeId) { - return ; + return ( + + ); } return ( {RULE_FORM_ROUTE_PARAMS_ERROR_TITLE}} - > - -

{RULE_FORM_ROUTE_PARAMS_ERROR_TEXT}

-
-
+ body={ + +

{RULE_FORM_ROUTE_PARAMS_ERROR_TEXT}

+
+ } + /> ); - }, [id, ruleTypeId, plugins, returnUrl]); + }, [id, ruleTypeId, plugins, onCancel, onSubmit]); return {ruleFormComponent}; }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx index 81d1aab4b2c3f..d8e6380462f9b 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.test.tsx @@ -76,6 +76,8 @@ const initialState: RuleFormState = { selectedRuleType: indexThresholdRuleType, selectedRuleTypeModel: indexThresholdRuleTypeModel, multiConsumerSelection: 'stackAlerts', + availableRuleTypes: [], + validConsumers: [], connectors: [], connectorTypes: [], aadTemplateFields: [], diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts index a65842125b6a8..d79ae00988875 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts @@ -8,7 +8,7 @@ */ import { RuleActionParams } from '@kbn/alerting-types'; -import { omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { RuleFormActionsErrors, RuleFormParamsErrors, RuleUiAction } from '../../common'; import { RuleFormData, RuleFormState } from '../types'; import { validateRuleBase, validateRuleParams } from '../validation'; @@ -106,13 +106,20 @@ export type RuleFormStateReducerAction = uuid: string; errors: RuleFormParamsErrors; }; + } + | { + type: 'runValidation'; }; const getUpdateWithValidation = (ruleFormState: RuleFormState) => (updater: () => RuleFormData): RuleFormState => { - const { minimumScheduleInterval, selectedRuleTypeModel, multiConsumerSelection } = - ruleFormState; + const { + minimumScheduleInterval, + selectedRuleTypeModel, + multiConsumerSelection, + selectedRuleType, + } = ruleFormState; const formData = updater(); @@ -121,17 +128,33 @@ const getUpdateWithValidation = ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), }; + const baseErrors = validateRuleBase({ + formData: formDataWithMultiConsumer, + minimumScheduleInterval, + }); + + const paramsErrors = validateRuleParams({ + formData: formDataWithMultiConsumer, + ruleTypeModel: selectedRuleTypeModel, + }); + + // We need to do this because the Missing Monitor Data rule type + // for whatever reason does not initialize the params with any data, + // therefore the expression component renders as blank + if (selectedRuleType.id === 'monitoring_alert_missing_monitoring_data') { + if (isEmpty(formData.params) && !isEmpty(paramsErrors)) { + Object.keys(paramsErrors).forEach((key) => { + formData.params[key] = null; + }); + } + } + return { ...ruleFormState, formData, - baseErrors: validateRuleBase({ - formData: formDataWithMultiConsumer, - minimumScheduleInterval, - }), - paramsErrors: validateRuleParams({ - formData: formDataWithMultiConsumer, - ruleTypeModel: selectedRuleTypeModel, - }), + baseErrors, + paramsErrors, + touched: true, }; }; @@ -222,6 +245,7 @@ export const ruleFormStateReducer = ( return { ...ruleFormState, multiConsumerSelection: payload, + touched: true, }; } case 'setMetadata': { @@ -326,6 +350,9 @@ export const ruleFormStateReducer = ( }, }; } + case 'runValidation': { + return updateWithValidation(() => formData); + } default: { return ruleFormState; } diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.test.tsx index ca80c0b77aae3..ac07c580fbd49 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.test.tsx @@ -61,6 +61,8 @@ const formDataMock: RuleFormData = { }, }; +const onCancel = jest.fn(); + useRuleFormState.mockReturnValue({ plugins: { application: { @@ -84,7 +86,6 @@ useRuleFormState.mockReturnValue({ }); const onSave = jest.fn(); -const returnUrl = 'management'; describe('rulePage', () => { afterEach(() => { @@ -92,7 +93,7 @@ describe('rulePage', () => { }); test('renders correctly', () => { - render(); + render(); expect(screen.getByText(RULE_FORM_PAGE_RULE_DEFINITION_TITLE)).toBeInTheDocument(); expect(screen.getByText(RULE_FORM_PAGE_RULE_ACTIONS_TITLE)).toBeInTheDocument(); @@ -100,7 +101,7 @@ describe('rulePage', () => { }); test('should call onSave when save button is pressed', () => { - render(); + render(); fireEvent.click(screen.getByTestId('rulePageFooterSaveButton')); fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); @@ -112,16 +113,16 @@ describe('rulePage', () => { }); test('should call onCancel when the cancel button is clicked', () => { - render(); + render(); fireEvent.click(screen.getByTestId('rulePageFooterCancelButton')); - expect(navigateToUrl).toHaveBeenCalledWith('management'); + expect(onCancel).toHaveBeenCalled(); }); test('should call onCancel when the return button is clicked', () => { - render(); + render(); fireEvent.click(screen.getByTestId('rulePageReturnButton')); - expect(navigateToUrl).toHaveBeenCalledWith('management'); + expect(onCancel).toHaveBeenCalled(); }); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx index 4e2e019d41269..68ff6d5db6b19 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiPageTemplate, EuiHorizontalRule, @@ -18,6 +18,8 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, + EuiCallOut, + EuiConfirmModal, } from '@elastic/eui'; import { RuleDefinition, @@ -33,32 +35,45 @@ import { RULE_FORM_PAGE_RULE_ACTIONS_TITLE, RULE_FORM_PAGE_RULE_DETAILS_TITLE, RULE_FORM_RETURN_TITLE, + DISABLED_ACTIONS_WARNING_TITLE, + RULE_FORM_CANCEL_MODAL_TITLE, + RULE_FORM_CANCEL_MODAL_DESCRIPTION, + RULE_FORM_CANCEL_MODAL_CONFIRM, + RULE_FORM_CANCEL_MODAL_CANCEL, } from '../translations'; +import { hasActionsError, hasActionsParamsErrors, hasParamsErrors } from '../validation'; +import { checkActionFormActionTypeEnabled } from '../utils/check_action_type_enabled'; export interface RulePageProps { isEdit?: boolean; isSaving?: boolean; - returnUrl: string; + onCancel?: () => void; onSave: (formData: RuleFormData) => void; } export const RulePage = (props: RulePageProps) => { - const { isEdit = false, isSaving = false, returnUrl, onSave } = props; + const { isEdit = false, isSaving = false, onCancel = () => {}, onSave } = props; + const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); const { plugins: { application }, + baseErrors = {}, + paramsErrors = {}, + actionsErrors = {}, + actionsParamsErrors = {}, formData, multiConsumerSelection, + connectorTypes, + connectors, + touched, } = useRuleFormState(); + const { actions } = formData; + const canReadConnectors = !!application.capabilities.actions?.show; const styles = useEuiBackgroundColorCSS().transparent; - const onCancel = useCallback(() => { - application.navigateToUrl(returnUrl); - }, [application, returnUrl]); - const onSaveInternal = useCallback(() => { onSave({ ...formData, @@ -66,11 +81,51 @@ export const RulePage = (props: RulePageProps) => { }); }, [onSave, formData, multiConsumerSelection]); - const actionComponent = useMemo(() => { + const onCancelInternal = useCallback(() => { + if (touched) { + setIsCancelModalOpen(true); + } else { + onCancel(); + } + }, [touched, onCancel]); + + const hasActionsDisabled = useMemo(() => { + const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured); + return actions.some((action) => { + const actionType = connectorTypes.find(({ id }) => id === action.actionTypeId); + if (!actionType) { + return false; + } + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionType, + preconfiguredConnectors + ); + return !actionType.enabled && !checkEnabledResult.isEnabled; + }); + }, [actions, connectors, connectorTypes]); + + const hasRuleDefinitionErrors = useMemo(() => { + return !!( + hasParamsErrors(paramsErrors) || + baseErrors.interval?.length || + baseErrors.alertDelay?.length + ); + }, [paramsErrors, baseErrors]); + + const hasActionErrors = useMemo(() => { + return hasActionsError(actionsErrors) || hasActionsParamsErrors(actionsParamsErrors); + }, [actionsErrors, actionsParamsErrors]); + + const hasRuleDetailsError = useMemo(() => { + return baseErrors.name?.length || baseErrors.tags?.length; + }, [baseErrors]); + + const actionComponent: EuiStepsProps['steps'] = useMemo(() => { if (canReadConnectors) { return [ { title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE, + status: hasActionErrors ? 'danger' : undefined, children: ( <> @@ -82,17 +137,19 @@ export const RulePage = (props: RulePageProps) => { ]; } return []; - }, [canReadConnectors]); + }, [hasActionErrors, canReadConnectors]); const steps: EuiStepsProps['steps'] = useMemo(() => { return [ { title: RULE_FORM_PAGE_RULE_DEFINITION_TITLE, + status: hasRuleDefinitionErrors ? 'danger' : undefined, children: , }, ...actionComponent, { title: RULE_FORM_PAGE_RULE_DETAILS_TITLE, + status: hasRuleDetailsError ? 'danger' : undefined, children: ( <> @@ -102,46 +159,73 @@ export const RulePage = (props: RulePageProps) => { ), }, ]; - }, [actionComponent]); + }, [hasRuleDefinitionErrors, hasRuleDetailsError, actionComponent]); return ( - - - + + + + + + {RULE_FORM_RETURN_TITLE} + + + + + + + + + + {hasActionsDisabled && ( + <> + + + + )} + + + + + + + {isCancelModalOpen && ( + setIsCancelModalOpen(false)} + onConfirm={onCancel} + buttonColor="danger" + defaultFocusedButton="confirm" + title={RULE_FORM_CANCEL_MODAL_TITLE} + confirmButtonText={RULE_FORM_CANCEL_MODAL_CONFIRM} + cancelButtonText={RULE_FORM_CANCEL_MODAL_CANCEL} > - - - {RULE_FORM_RETURN_TITLE} - - - - - - - - - - - - - - - +

{RULE_FORM_CANCEL_MODAL_DESCRIPTION}

+ + )} + ); }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx index 45e2008773583..d937c60aa3a52 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx @@ -32,15 +32,27 @@ const onSave = jest.fn(); const onCancel = jest.fn(); hasRuleErrors.mockReturnValue(false); -useRuleFormState.mockReturnValue({ - baseErrors: {}, - paramsErrors: {}, - formData: { - actions: [], - }, -}); describe('rulePageFooter', () => { + beforeEach(() => { + useRuleFormState.mockReturnValue({ + plugins: { + application: { + capabilities: { + actions: { + show: true, + }, + }, + }, + }, + baseErrors: {}, + paramsErrors: {}, + formData: { + actions: [], + }, + }); + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -75,6 +87,30 @@ describe('rulePageFooter', () => { expect(screen.getByTestId('rulePageConfirmCreateRule')).toBeInTheDocument(); }); + test('should not show creat rule confirmation if user cannot read actions', () => { + useRuleFormState.mockReturnValue({ + plugins: { + application: { + capabilities: { + actions: { + show: false, + }, + }, + }, + }, + baseErrors: {}, + paramsErrors: {}, + formData: { + actions: [], + }, + }); + + render(); + fireEvent.click(screen.getByTestId('rulePageFooterSaveButton')); + expect(screen.queryByTestId('rulePageConfirmCreateRule')).not.toBeInTheDocument(); + expect(onSave).toHaveBeenCalled(); + }); + test('should show call onSave if clicking rule confirmation', () => { render(); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx index 09d2ac429fd50..62a0e4b64e4f1 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx @@ -34,6 +34,7 @@ export const RulePageFooter = (props: RulePageFooterProps) => { const { isEdit = false, isSaving = false, onCancel, onSave } = props; const { + plugins: { application }, formData: { actions }, connectors, baseErrors = {}, @@ -78,11 +79,12 @@ export const RulePageFooter = (props: RulePageFooterProps) => { if (isEdit) { return onSave(); } - if (actions.length === 0) { + const canReadConnectors = !!application.capabilities.actions?.show; + if (actions.length === 0 && canReadConnectors) { return setShowCreateConfirmation(true); } onSave(); - }, [actions, isEdit, onSave]); + }, [actions, isEdit, application, onSave]); const onCreateConfirmClick = useCallback(() => { setShowCreateConfirmation(false); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts index 20e87c66f10f4..fca2e30b94434 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts @@ -194,7 +194,7 @@ export const RULE_TYPE_REQUIRED_TEXT = i18n.translate( export const RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT = i18n.translate( 'alertsUIShared.ruleForm.error.belowMinimumAlertDelayText', { - defaultMessage: 'Alert delay must be greater than 1.', + defaultMessage: 'Alert delay must be 1 or greater.', } ); @@ -498,6 +498,34 @@ export const RULE_FORM_RETURN_TITLE = i18n.translate('alertsUIShared.ruleForm.re defaultMessage: 'Return', }); +export const RULE_FORM_CANCEL_MODAL_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalTitle', + { + defaultMessage: 'Discard unsaved changes to rule?', + } +); + +export const RULE_FORM_CANCEL_MODAL_DESCRIPTION = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalDescription', + { + defaultMessage: "You can't recover unsaved changes.", + } +); + +export const RULE_FORM_CANCEL_MODAL_CONFIRM = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalConfirm', + { + defaultMessage: 'Discard changes', + } +); + +export const RULE_FORM_CANCEL_MODAL_CANCEL = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalCancel', + { + defaultMessage: 'Cancel', + } +); + export const MODAL_SEARCH_PLACEHOLDER = i18n.translate( 'alertsUIShared.ruleForm.modalSearchPlaceholder', { @@ -586,3 +614,10 @@ export const TECH_PREVIEW_DESCRIPTION = i18n.translate( 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', } ); + +export const DISABLED_ACTIONS_WARNING_TITLE = i18n.translate( + 'alertsUIShared.disabledActionsWarningTitle', + { + defaultMessage: 'This rule has actions that are disabled', + } +); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts index d33c74da528db..4b45f64d3ead4 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts @@ -72,6 +72,7 @@ export interface RuleFormState { connectors: ActionConnector[]; connectorTypes: ActionType[]; aadTemplateFields: ActionVariable[]; + availableRuleTypes: RuleTypeWithDescription[]; baseErrors?: RuleFormBaseErrors; paramsErrors?: RuleFormParamsErrors; actionsErrors?: Record; @@ -83,8 +84,9 @@ export interface RuleFormState { metadata?: Record; minimumScheduleInterval?: MinimumScheduleInterval; canShowConsumerSelection?: boolean; - validConsumers?: RuleCreationValidConsumer[]; + validConsumers: RuleCreationValidConsumer[]; flappingSettings?: RulesSettingsFlapping; + touched?: boolean; } export type InitialRule = Partial & diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts index 217bb18328d0e..0b5234c669440 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts @@ -17,9 +17,6 @@ export const getAuthorizedConsumers = ({ ruleType: RuleTypeWithDescription; validConsumers: RuleCreationValidConsumer[]; }) => { - if (!ruleType.authorizedConsumers) { - return []; - } return Object.entries(ruleType.authorizedConsumers).reduce( (result, [authorizedConsumer, privilege]) => { if ( diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_default_params.ts similarity index 52% rename from src/plugins/saved_objects_tagging_oss/public/decorator/index.ts rename to packages/kbn-alerts-ui-shared/src/rule_form/utils/get_default_params.ts index e2c9f0dcf14cf..d2aab787d6eb5 100644 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/index.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_default_params.ts @@ -6,15 +6,20 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ +import { ActionTypeModel, RuleTypeWithDescription } from '../../common/types'; -import { SavedObjectDecoratorConfig } from '@kbn/saved-objects-plugin/public'; -import { tagDecoratorFactory, decoratorId } from './factory'; -import { InternalTagDecoratedSavedObject } from './types'; - -export type { TagDecoratedSavedObject } from './types'; - -export const tagDecoratorConfig: SavedObjectDecoratorConfig = { - id: decoratorId, - priority: 100, - factory: tagDecoratorFactory, +export const getDefaultParams = ({ + group, + ruleType, + actionTypeModel, +}: { + group: string; + actionTypeModel: ActionTypeModel; + ruleType: RuleTypeWithDescription; +}) => { + if (group === ruleType.recoveryActionGroup.id) { + return actionTypeModel.defaultRecoveredActionParams; + } else { + return actionTypeModel.defaultActionParams; + } }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts index f5b583a1a9c63..53c9aedda7545 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts @@ -17,3 +17,4 @@ export * from './get_initial_schedule'; export * from './has_fields_for_aad'; export * from './get_selected_action_group'; export * from './get_initial_consumer'; +export * from './get_default_params'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts b/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts index d65e9c5893937..57afe66b53edf 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts @@ -35,7 +35,10 @@ export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormAc if ('alertsFilter' in action) { const query = action?.alertsFilter?.query; - if (query && !query.kql) { + if (!query) { + return errors; + } + if (!query.filters.length && !query.kql) { errors.filterQuery.push( i18n.translate('alertsUIShared.ruleForm.actionsForm.requiredFilterQuery', { defaultMessage: 'A custom query is required.', @@ -43,7 +46,6 @@ export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormAc ); } } - return errors; }; @@ -88,11 +90,7 @@ export function validateRuleBase({ errors.ruleTypeId.push(RULE_TYPE_REQUIRED_TEXT); } - if ( - formData.alertDelay && - !isNaN(formData.alertDelay?.active) && - formData.alertDelay?.active < 1 - ) { + if (!formData.alertDelay || isNaN(formData.alertDelay.active) || formData.alertDelay.active < 1) { errors.alertDelay.push(RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT); } @@ -111,34 +109,41 @@ export const validateRuleParams = ({ return ruleTypeModel.validate(formData.params, isServerless).errors; }; -const hasRuleBaseErrors = (errors: RuleFormBaseErrors) => { +export const hasRuleBaseErrors = (errors: RuleFormBaseErrors) => { return Object.values(errors).some((error: string[]) => error.length > 0); }; -const hasActionsError = (actionsErrors: Record) => { +export const hasActionsError = (actionsErrors: Record) => { return Object.values(actionsErrors).some((errors: RuleFormActionsErrors) => { return Object.values(errors).some((error: string[]) => error.length > 0); }); }; -const hasParamsErrors = (errors: RuleFormParamsErrors): boolean => { - const values = Object.values(errors); +export const hasParamsErrors = (errors: RuleFormParamsErrors | string | string[]): boolean => { let hasError = false; - for (const value of values) { - if (Array.isArray(value) && value.length > 0) { - return true; - } - if (typeof value === 'string' && value.trim() !== '') { - return true; - } - if (isObject(value)) { - hasError = hasParamsErrors(value as RuleFormParamsErrors); - } + + if (typeof errors === 'string' && errors.trim() !== '') { + hasError = true; } + + if (Array.isArray(errors)) { + errors.forEach((error) => { + hasError = hasError || hasParamsErrors(error); + }); + } + + if (isObject(errors)) { + Object.entries(errors).forEach(([_, value]) => { + hasError = hasError || hasParamsErrors(value); + }); + } + return hasError; }; -const hasActionsParamsErrors = (actionsParamsErrors: Record) => { +export const hasActionsParamsErrors = ( + actionsParamsErrors: Record +) => { return Object.values(actionsParamsErrors).some((errors: RuleFormParamsErrors) => { return hasParamsErrors(errors); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx index 99f64f0a3977f..030cde8127b0a 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx @@ -218,15 +218,17 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = direction={isDesktop ? 'row' : 'column'} alignItems={isDesktop ? 'center' : undefined} > - + {flappingLabel} - + {enabled ? flappingOnLabel : flappingOffLabel} {flappingSettings && enabled && ( - {flappingOverrideLabel} + + {flappingOverrideLabel} + )} @@ -236,6 +238,7 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = compressed checked={!!flappingSettings} label={flappingOverrideConfiguration} + disabled={!canWriteFlappingSettingsUI} onChange={onFlappingToggle} /> )} @@ -256,6 +259,7 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = spaceFlappingSettings, flappingSettings, flappingOffTooltip, + canWriteFlappingSettingsUI, onFlappingToggle, ]); @@ -273,12 +277,14 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = statusChangeThreshold={flappingSettings.statusChangeThreshold} onLookBackWindowChange={onLookBackWindowChange} onStatusChangeThresholdChange={onStatusChangeThresholdChange} + isDisabled={!canWriteFlappingSettingsUI} /> ); }, [ flappingSettings, spaceFlappingSettings, + canWriteFlappingSettingsUI, onLookBackWindowChange, onStatusChangeThresholdChange, ]); diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx index 2a5cc4186013d..149eb5b792c1b 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx @@ -80,6 +80,7 @@ export const RuleSettingsFlappingTitleTooltip = (props: RuleSettingsFlappingTitl panelStyle={{ width: 500, }} + closePopover={() => setIsPopoverOpen(false)} button={ { +): Filter => { if (!field || !operator) { return updateField(filter, field); } @@ -35,7 +35,7 @@ export const updateFilter = ( return updateWithIsOperator(filter, operator, params, fieldType); }; -function updateField(filter: Filter, field?: string) { +function updateField(filter: Filter, field?: string): Filter { return { ...filter, meta: { @@ -48,7 +48,7 @@ function updateField(filter: Filter, field?: string) { type: undefined, }, query: undefined, - }; + } as Filter; // need the casting because `field` shouldn't be there } function updateWithExistsOperator(filter: Filter, operator?: FilterMeta) { @@ -104,7 +104,7 @@ function updateWithRangeOperator( operator: FilterMeta, rawParams: Filter['meta']['params'] | undefined, field: string -) { +): Filter { if (isRangeFilterParams(rawParams)) { const { from, to } = rawParams; const params = { @@ -148,7 +148,7 @@ function updateWithRangeOperator( }, }, }; - return updatedFilter; + return updatedFilter as Filter; // need the casting because it doesn't like the types of `params.gte|lt` } } diff --git a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml index 3c008407d5c46..e9223cd5d73ef 100644 --- a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml +++ b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml @@ -55,6 +55,7 @@ viewer: - feature_dashboard.all - feature_maps.all - feature_visualize.all + - feature_dataQuality.all resources: '*' run_as: [] diff --git a/packages/kbn-esql-editor/src/esql_editor.tsx b/packages/kbn-esql-editor/src/esql_editor.tsx index abdaa577c4bea..97340dc20d422 100644 --- a/packages/kbn-esql-editor/src/esql_editor.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.tsx @@ -321,13 +321,10 @@ export const ESQLEditor = memo(function ESQLEditor({ }, []); const { cache: dataSourcesCache, memoizedSources } = useMemo(() => { - const fn = memoize( - (...args: [DataViewsPublicPluginStart, CoreStart]) => ({ - timestamp: Date.now(), - result: getESQLSources(...args), - }), - ({ esql }) => esql - ); + const fn = memoize((...args: [DataViewsPublicPluginStart, CoreStart]) => ({ + timestamp: Date.now(), + result: getESQLSources(...args), + })); return { cache: fn.cache, memoizedSources: fn }; }, []); diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts index 9addfdf22da01..6452c2cf3c2cc 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts @@ -131,7 +131,7 @@ describe('processVersionedRouter', () => { {} ); - expect(Object.keys(get(baseCase, 'paths["/foo"].get.responses.200.content'))).toEqual([ + expect(Object.keys(get(baseCase, 'paths["/foo"].get.responses.200.content')!)).toEqual([ 'application/test+json; Elastic-Api-Version=2023-10-31', 'application/test+json; Elastic-Api-Version=2024-12-31', ]); @@ -142,7 +142,7 @@ describe('processVersionedRouter', () => { createOperationIdCounter(), { version: '2023-10-31' } ); - expect(Object.keys(get(filteredCase, 'paths["/foo"].get.responses.200.content'))).toEqual([ + expect(Object.keys(get(filteredCase, 'paths["/foo"].get.responses.200.content')!)).toEqual([ 'application/test+json; Elastic-Api-Version=2023-10-31', ]); }); diff --git a/packages/kbn-rule-data-utils/src/routes/stack_rule_paths.ts b/packages/kbn-rule-data-utils/src/routes/stack_rule_paths.ts index 956889028269b..216a3e402c8ee 100644 --- a/packages/kbn-rule-data-utils/src/routes/stack_rule_paths.ts +++ b/packages/kbn-rule-data-utils/src/routes/stack_rule_paths.ts @@ -9,5 +9,10 @@ export const ruleDetailsRoute = '/rule/:ruleId' as const; export const triggersActionsRoute = '/app/management/insightsAndAlerting/triggersActions' as const; +export const createRuleRoute = '/rules/create/:ruleTypeId' as const; +export const editRuleRoute = '/rules/edit/:id' as const; export const getRuleDetailsRoute = (ruleId: string) => ruleDetailsRoute.replace(':ruleId', ruleId); +export const getCreateRuleRoute = (ruleTypeId: string) => + createRuleRoute.replace(':ruleTypeId', ruleTypeId); +export const getEditRuleRoute = (ruleId: string) => editRuleRoute.replace(':id', ruleId); diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx index 34cb1a4b0ed7a..8cb83176a6591 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx @@ -45,6 +45,7 @@ interface ConnectorConfigurationProps { hasPlatinumLicense: boolean; isLoading: boolean; saveConfig: (configuration: Record) => void; + saveAndSync?: (configuration: Record) => void; stackManagementLink?: string; subscriptionLink?: string; children?: React.ReactNode; @@ -90,6 +91,7 @@ export const ConnectorConfigurationComponent: FC< hasPlatinumLicense, isLoading, saveConfig, + saveAndSync, subscriptionLink, stackManagementLink, }) => { @@ -166,6 +168,12 @@ export const ConnectorConfigurationComponent: FC< saveConfig(config); setIsEditing(false); }} + {...(saveAndSync && { + saveAndSync: (config) => { + saveAndSync(config); + setIsEditing(false); + }, + })} /> ) : ( uncategorizedDisplayList.length > 0 && ( diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx index f7e619f407f12..9b83f7c0d3302 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx @@ -36,6 +36,7 @@ interface ConnectorConfigurationForm { isLoading: boolean; isNative: boolean; saveConfig: (config: Record) => void; + saveAndSync?: (config: Record) => void; stackManagementHref?: string; subscriptionLink?: string; } @@ -60,6 +61,7 @@ export const ConnectorConfigurationForm: React.FC = isLoading, isNative, saveConfig, + saveAndSync, }) => { const [localConfig, setLocalConfig] = useState(configuration); const [configView, setConfigView] = useState( @@ -167,19 +169,7 @@ export const ConnectorConfigurationForm: React.FC = )} - - - - {i18n.translate('searchConnectors.configurationConnector.config.submitButton.title', { - defaultMessage: 'Save configuration', - })} - - + = )} + + + {i18n.translate('searchConnectors.configurationConnector.config.submitButton.title', { + defaultMessage: 'Save', + })} + + + {saveAndSync && ( + + { + saveAndSync(configViewToConfigValues(configView)); + }} + > + {i18n.translate( + 'searchConnectors.configurationConnector.config.submitButton.title', + { + defaultMessage: 'Save and sync', + } + )} + + + )} diff --git a/src/plugins/controls/server/options_list/suggestion_queries/options_list_search_suggestions.ts b/src/plugins/controls/server/options_list/suggestion_queries/options_list_search_suggestions.ts index 7bd20eb350dde..aa41ef1575c69 100644 --- a/src/plugins/controls/server/options_list/suggestion_queries/options_list_search_suggestions.ts +++ b/src/plugins/controls/server/options_list/suggestion_queries/options_list_search_suggestions.ts @@ -117,13 +117,11 @@ const suggestionAggSubtypes: { [key: string]: OptionsListSuggestionAggregationBu const isNested = fieldSpec && getFieldSubtypeNested(fieldSpec); basePath += isNested ? '.nestedSuggestions.filteredSuggestions' : '.filteredSuggestions'; - const suggestions = get(rawEsResult, `${basePath}.suggestions.buckets`)?.reduce( - (acc: OptionsListSuggestions, suggestion: EsBucket) => { - acc.push({ value: suggestion.key, docCount: suggestion.doc_count }); - return acc; - }, - [] - ); + const buckets = get(rawEsResult, `${basePath}.suggestions.buckets`, []) as EsBucket[]; + const suggestions = buckets.reduce((acc: OptionsListSuggestions, suggestion: EsBucket) => { + acc.push({ value: suggestion.key, docCount: suggestion.doc_count }); + return acc; + }, []); return { suggestions, totalCardinality: get(rawEsResult, `${basePath}.unique_terms.value`), diff --git a/src/plugins/data_views/common/data_views/data_views.test.ts b/src/plugins/data_views/common/data_views/data_views.test.ts index 8d9e2fdbc148b..917419a8457bf 100644 --- a/src/plugins/data_views/common/data_views/data_views.test.ts +++ b/src/plugins/data_views/common/data_views/data_views.test.ts @@ -8,7 +8,7 @@ */ import { set } from '@kbn/safer-lodash-set'; -import { defaults, get } from 'lodash'; +import { defaults } from 'lodash'; import { DataViewsService, DataView, DataViewLazy } from '.'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; @@ -297,7 +297,8 @@ describe('IndexPatterns', () => { test('does cache ad-hoc data views', async () => { const id = '1'; - const createFromSpecOriginal = get(indexPatterns, 'createFromSpec'); + // eslint-disable-next-line dot-notation + const createFromSpecOriginal = indexPatterns['createFromSpec']; let mockedCreateFromSpec: jest.Mock; set( @@ -340,7 +341,8 @@ describe('IndexPatterns', () => { test('does cache ad-hoc data views for DataViewLazy', async () => { const id = '1'; - const createFromSpecOriginal = get(indexPatterns, 'createFromSpecLazy'); + // eslint-disable-next-line dot-notation + const createFromSpecOriginal = indexPatterns['createFromSpecLazy']; let mockedCreateFromSpec: jest.Mock; set( diff --git a/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.ts b/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.ts index 83a6673412756..60dbc9f1fe092 100644 --- a/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.ts +++ b/src/plugins/expressions/public/react_expression_renderer/use_debounced_value.ts @@ -8,7 +8,7 @@ */ import { debounce } from 'lodash'; -import type { Cancelable } from 'lodash'; +import type { DebouncedFunc } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; @@ -22,7 +22,7 @@ export function useDebouncedValue(value: T, timeout?: number): [T, boolean] { }, [setStoredValue, setPending] ); - const setDebouncedValue = useMemo>( + const setDebouncedValue = useMemo>>( () => (timeout ? debounce(setValue, timeout) : setValue), [setValue, timeout] ); diff --git a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 5389f03dff11f..c0f980f2badb2 100644 --- a/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -82,7 +82,7 @@ export class PhraseFilterManager extends FilterManager { private getValueFromFilter(kbnFilter: Filter): any { // bool filter - multiple phrase filters if (_.has(kbnFilter, 'query.bool.should')) { - return _.get(kbnFilter, 'query.bool.should') + return (_.get(kbnFilter, 'query.bool.should') as PhraseFilter[]) .map((kbnQueryFilter: PhraseFilter) => { return this.getValueFromFilter(kbnQueryFilter); }) diff --git a/src/plugins/saved_objects/kibana.jsonc b/src/plugins/saved_objects/kibana.jsonc index aa1c9aae31194..1f063a7cdfa59 100644 --- a/src/plugins/saved_objects/kibana.jsonc +++ b/src/plugins/saved_objects/kibana.jsonc @@ -9,9 +9,6 @@ "requiredPlugins": [ "data", "dataViews" - ], - "requiredBundles": [ - "kibanaUtils" ] } } diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 4ce1d679c743f..6d7a013cf59ca 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -11,13 +11,7 @@ import { SavedObjectsPublicPlugin } from './plugin'; export type { OnSaveProps, OriginSaveModalProps, SaveModalState, SaveResult } from './save_modal'; export { SavedObjectSaveModal, SavedObjectSaveModalOrigin, showSaveModal } from './save_modal'; -export type { - SavedObjectDecorator, - SavedObjectDecoratorFactory, - SavedObjectDecoratorConfig, -} from './saved_object'; export { checkForDuplicateTitle, saveWithConfirmation, isErrorNonFatal } from './saved_object'; export type { SavedObjectSaveOpts, SavedObject, SavedObjectConfig } from './types'; -export type { SavedObjectsStart, SavedObjectSetup } from './plugin'; export const plugin = () => new SavedObjectsPublicPlugin(); diff --git a/src/plugins/saved_objects/public/mocks.ts b/src/plugins/saved_objects/public/mocks.ts index 124f9bcd2d659..5d15efcb0b22f 100644 --- a/src/plugins/saved_objects/public/mocks.ts +++ b/src/plugins/saved_objects/public/mocks.ts @@ -7,21 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { SavedObjectsStart, SavedObjectSetup } from './plugin'; - -const createStartContract = (): SavedObjectsStart => { - return { - SavedObjectClass: jest.fn(), - }; -}; - -const createSetupContract = (): jest.Mocked => { - return { - registerDecorator: jest.fn(), - }; -}; - -export const savedObjectsPluginMock = { - createStartContract, - createSetupContract, -}; +export const savedObjectsPluginMock = {}; diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index cbf3a5e69cf7f..fd67e783fab75 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -12,55 +12,19 @@ import { CoreStart, Plugin } from '@kbn/core/public'; import './index.scss'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { - createSavedObjectClass, - SavedObjectDecoratorRegistry, - SavedObjectDecoratorConfig, -} from './saved_object'; -import { SavedObject } from './types'; import { setStartServices } from './kibana_services'; -export interface SavedObjectSetup { - registerDecorator: (config: SavedObjectDecoratorConfig) => void; -} - -export interface SavedObjectsStart { - /** - * @deprecated - * @removeBy 8.8.0 - */ - SavedObjectClass: new (raw: Record) => SavedObject; -} - export interface SavedObjectsStartDeps { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; } -export class SavedObjectsPublicPlugin - implements Plugin -{ - private decoratorRegistry = new SavedObjectDecoratorRegistry(); - - public setup(): SavedObjectSetup { - return { - registerDecorator: (config) => this.decoratorRegistry.register(config), - }; +export class SavedObjectsPublicPlugin implements Plugin<{}, {}, object, SavedObjectsStartDeps> { + public setup() { + return {}; } public start(core: CoreStart, { data, dataViews }: SavedObjectsStartDeps) { setStartServices(core); - return { - SavedObjectClass: createSavedObjectClass( - { - dataViews, - savedObjectsClient: core.savedObjects.client, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, - }, - core, - this.decoratorRegistry - ), - }; + return {}; } } diff --git a/src/plugins/saved_objects/public/saved_object/decorators/index.ts b/src/plugins/saved_objects/public/saved_object/decorators/index.ts deleted file mode 100644 index 62f4f27bd2590..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/decorators/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { ISavedObjectDecoratorRegistry, SavedObjectDecoratorConfig } from './registry'; -export { SavedObjectDecoratorRegistry } from './registry'; -export type { SavedObjectDecorator, SavedObjectDecoratorFactory } from './types'; diff --git a/src/plugins/saved_objects/public/saved_object/decorators/registry.mock.ts b/src/plugins/saved_objects/public/saved_object/decorators/registry.mock.ts deleted file mode 100644 index ad292b65e4e51..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/decorators/registry.mock.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ISavedObjectDecoratorRegistry } from './registry'; - -const createRegistryMock = () => { - const mock: jest.Mocked = { - register: jest.fn(), - getOrderedDecorators: jest.fn(), - }; - - mock.getOrderedDecorators.mockReturnValue([]); - - return mock; -}; - -export const savedObjectsDecoratorRegistryMock = { - create: createRegistryMock, -}; diff --git a/src/plugins/saved_objects/public/saved_object/decorators/registry.test.ts b/src/plugins/saved_objects/public/saved_object/decorators/registry.test.ts deleted file mode 100644 index 590e2717c48ff..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/decorators/registry.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectDecoratorRegistry } from './registry'; - -const mockDecorator = (id: string = 'foo') => { - return { - getId: () => id, - decorateConfig: () => undefined, - decorateObject: () => undefined, - }; -}; - -describe('SavedObjectDecoratorRegistry', () => { - let registry: SavedObjectDecoratorRegistry; - - beforeEach(() => { - registry = new SavedObjectDecoratorRegistry(); - }); - - describe('register', () => { - it('allow to register a decorator', () => { - expect(() => { - registry.register({ - id: 'foo', - priority: 9000, - factory: () => mockDecorator(), - }); - }).not.toThrow(); - }); - - it('throws when trying to register the same id twice', () => { - registry.register({ - id: 'foo', - priority: 9000, - factory: () => mockDecorator(), - }); - - expect(() => { - registry.register({ - id: 'foo', - priority: 42, - factory: () => mockDecorator(), - }); - }).toThrowErrorMatchingInlineSnapshot(`"A decorator is already registered for id foo"`); - }); - - it('throws when trying to register multiple decorators with the same priority', () => { - registry.register({ - id: 'foo', - priority: 100, - factory: () => mockDecorator(), - }); - - expect(() => { - registry.register({ - id: 'bar', - priority: 100, - factory: () => mockDecorator(), - }); - }).toThrowErrorMatchingInlineSnapshot(`"A decorator is already registered for priority 100"`); - }); - }); - - describe('getOrderedDecorators', () => { - it('returns the decorators in correct order', () => { - registry.register({ - id: 'A', - priority: 1000, - factory: () => mockDecorator('A'), - }); - registry.register({ - id: 'B', - priority: 100, - factory: () => mockDecorator('B'), - }); - registry.register({ - id: 'C', - priority: 2000, - factory: () => mockDecorator('C'), - }); - - const decorators = registry.getOrderedDecorators({} as any); - expect(decorators.map((d) => d.getId())).toEqual(['B', 'A', 'C']); - }); - - it('invoke the decorators factory with the provided services', () => { - const services = Symbol('services'); - - const decorator = { - id: 'foo', - priority: 9000, - factory: jest.fn(), - }; - registry.register(decorator); - registry.getOrderedDecorators(services as any); - - expect(decorator.factory).toHaveBeenCalledTimes(1); - expect(decorator.factory).toHaveBeenCalledWith(services); - }); - - it('invoke the factory each time the method is called', () => { - const services = Symbol('services'); - - const decorator = { - id: 'foo', - priority: 9000, - factory: jest.fn(), - }; - registry.register(decorator); - registry.getOrderedDecorators(services as any); - - expect(decorator.factory).toHaveBeenCalledTimes(1); - - registry.getOrderedDecorators(services as any); - - expect(decorator.factory).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/src/plugins/saved_objects/public/saved_object/decorators/registry.ts b/src/plugins/saved_objects/public/saved_object/decorators/registry.ts deleted file mode 100644 index 816ba2a84c1ae..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/decorators/registry.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObjectDecoratorFactory } from './types'; -import { SavedObjectKibanaServices, SavedObject } from '../../types'; - -export interface SavedObjectDecoratorConfig { - /** - * The id of the decorator - */ - id: string; - /** - * Highest priority will be called **last** - * (the decoration will be at the highest level) - */ - priority: number; - /** - * The factory to use to create the decorator - */ - factory: SavedObjectDecoratorFactory; -} - -export type ISavedObjectDecoratorRegistry = PublicMethodsOf; - -export class SavedObjectDecoratorRegistry { - private readonly registry = new Map>(); - - public register(config: SavedObjectDecoratorConfig) { - if (this.registry.has(config.id)) { - throw new Error(`A decorator is already registered for id ${config.id}`); - } - if ([...this.registry.values()].find(({ priority }) => priority === config.priority)) { - throw new Error(`A decorator is already registered for priority ${config.priority}`); - } - this.registry.set(config.id, config); - } - - public getOrderedDecorators(services: SavedObjectKibanaServices) { - return [...this.registry.values()] - .sort((a, b) => a.priority - b.priority) - .map(({ factory }) => factory(services)); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/decorators/types.ts b/src/plugins/saved_objects/public/saved_object/decorators/types.ts deleted file mode 100644 index 8f992a46a8fb9..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/decorators/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObject, SavedObjectKibanaServices, SavedObjectConfig } from '../../types'; - -export interface SavedObjectDecorator { - /** - * Id of the decorator - */ - getId(): string; - - /** - * Decorate the saved object provided config. This can be used to enhance or alter the object's provided - * configuration. - */ - decorateConfig: (config: SavedObjectConfig) => void; - /** - * Decorate the saved object instance. Can be used to add additional methods to it. - * - * @remarks This will be called before the internal constructor of the object, meaning that - * wrapping existing methods is not possible (and is not a desired pattern). - */ - decorateObject: (object: T) => void; -} - -export type SavedObjectDecoratorFactory = ( - services: SavedObjectKibanaServices -) => SavedObjectDecorator; diff --git a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts deleted file mode 100644 index 388f84fcd524d..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { once } from 'lodash'; -import { hydrateIndexPattern } from './hydrate_index_pattern'; -import { intializeSavedObject } from './initialize_saved_object'; -import { serializeSavedObject } from './serialize_saved_object'; - -import { - EsResponse, - SavedObject, - SavedObjectConfig, - SavedObjectKibanaServices, - SavedObjectSaveOpts, - StartServices, -} from '../../types'; -import { applyESResp } from './apply_es_resp'; -import { saveSavedObject } from './save_saved_object'; -import { SavedObjectDecorator } from '../decorators'; - -const applyDecorators = ( - object: SavedObject, - config: SavedObjectConfig, - decorators: SavedObjectDecorator[] -) => { - decorators.forEach((decorator) => { - decorator.decorateConfig(config); - decorator.decorateObject(object); - }); -}; - -export function buildSavedObject( - savedObject: SavedObject, - config: SavedObjectConfig, - services: SavedObjectKibanaServices, - startServices: StartServices, - decorators: SavedObjectDecorator[] = [] -) { - applyDecorators(savedObject, config, decorators); - - const { dataViews, savedObjectsClient } = services; - // type name for this object, used as the ES-type - const esType = config.type || ''; - - savedObject.getDisplayName = () => esType; - - // NOTE: this.type (not set in this file, but somewhere else) is the sub type, e.g. 'area' or - // 'data table', while esType is the more generic type - e.g. 'visualization' or 'saved search'. - savedObject.getEsType = () => esType; - - /** - * Flips to true during a save operation, and back to false once the save operation - * completes. - * @type {boolean} - */ - savedObject.isSaving = false; - savedObject.defaults = config.defaults || {}; - // optional search source which this object configures - savedObject.searchSource = config.searchSource - ? services.search.searchSource.createEmpty() - : undefined; - // the id of the document - savedObject.id = config.id || void 0; - // the migration version of the document, should only be set on imports - savedObject.migrationVersion = config.migrationVersion; - // Whether to create a copy when the object is saved. This should eventually go away - // in favor of a better rename/save flow. - savedObject.copyOnSave = false; - - /** - * After creation or fetching from ES, ensure that the searchSources index indexPattern - * is an bonafide IndexPattern object. - * - * @return {Promise} - */ - savedObject.hydrateIndexPattern = (id?: string) => - hydrateIndexPattern(id || '', savedObject, dataViews, config); - /** - * Asynchronously initialize this object - will only run - * once even if called multiple times. - * - * @return {Promise} - * @resolved {SavedObject} - */ - savedObject.init = once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); - - savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config, services); - - /** - * Serialize this object - * @return {Object} - */ - savedObject._serialize = () => serializeSavedObject(savedObject, config); - - /** - * Returns true if the object's original title has been changed. New objects return false. - * @return {boolean} - */ - savedObject.isTitleChanged = () => - savedObject._source && savedObject._source.title !== savedObject.title; - - savedObject.creationOpts = (opts: Record = {}) => ({ - id: savedObject.id, - migrationVersion: savedObject.migrationVersion, - ...opts, - }); - - savedObject.save = async (opts: SavedObjectSaveOpts) => { - try { - const result = await saveSavedObject(savedObject, config, opts, services, startServices); - return Promise.resolve(result); - } catch (e) { - return Promise.reject(e); - } - }; - - savedObject.destroy = () => {}; - - /** - * Delete this object from Elasticsearch - * @return {promise} - */ - savedObject.delete = () => { - if (!savedObject.id) { - return Promise.reject(new Error('Deleting a saved Object requires type and id')); - } - return savedObjectsClient.delete(esType, savedObject.id); - }; -} diff --git a/src/plugins/saved_objects/public/saved_object/index.ts b/src/plugins/saved_objects/public/saved_object/index.ts index 89cfa263093b1..560178fdf4f36 100644 --- a/src/plugins/saved_objects/public/saved_object/index.ts +++ b/src/plugins/saved_objects/public/saved_object/index.ts @@ -7,13 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { createSavedObjectClass } from './saved_object'; export { checkForDuplicateTitle } from './helpers/check_for_duplicate_title'; export { saveWithConfirmation } from './helpers/save_with_confirmation'; export { isErrorNonFatal } from './helpers/save_saved_object'; -export type { - SavedObjectDecoratorFactory, - SavedObjectDecorator, - SavedObjectDecoratorConfig, -} from './decorators'; -export { SavedObjectDecoratorRegistry } from './decorators'; diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts deleted file mode 100644 index 645ae855b277b..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ /dev/null @@ -1,888 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { createSavedObjectClass } from './saved_object'; -import { - SavedObject, - SavedObjectConfig, - SavedObjectKibanaServices, - SavedObjectSaveOpts, -} from '../types'; -import { SavedObjectDecorator } from './decorators'; - -import { - analyticsServiceMock, - coreMock, - i18nServiceMock, - themeServiceMock, -} from '@kbn/core/public/mocks'; -import { dataPluginMock, createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; -import { createStubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { SavedObjectAttributes, SimpleSavedObject } from '@kbn/core/public'; -import { DataView } from '@kbn/data-plugin/common'; -import { savedObjectsDecoratorRegistryMock } from './decorators/registry.mock'; - -describe('Saved Object', () => { - const startMock = coreMock.createStart(); - const dataStartMock = dataPluginMock.createStartContract(); - const saveOptionsMock = {} as SavedObjectSaveOpts; - const savedObjectsClientStub = startMock.savedObjects.client; - const startServices = { - analytics: analyticsServiceMock.createAnalyticsServiceStart(), - i18n: i18nServiceMock.createStartContract(), - theme: themeServiceMock.createStartContract(), - }; - let decoratorRegistry: ReturnType; - - let SavedObjectClass: new (config: SavedObjectConfig) => SavedObject; - - /** - * Returns a fake doc response with the given index and id, of type dashboard - * that can be used to stub es calls. - * @param indexPatternId - * @param additionalOptions - object that will be assigned to the mocked doc response. - * @returns {{attributes: {}, type: string, id: *, _version: string}} - */ - function getMockedDocResponse(indexPatternId: string, additionalOptions = {}) { - return { - type: 'dashboard', - id: indexPatternId, - _version: 'foo', - attributes: {}, - ...additionalOptions, - } as SimpleSavedObject; - } - - /** - * Stubs some of the es retrieval calls so it returns the given response. - * @param {Object} mockDocResponse - */ - function stubESResponse(mockDocResponse: SimpleSavedObject) { - // Stub out search for duplicate title: - savedObjectsClientStub.get = jest.fn().mockReturnValue(Promise.resolve(mockDocResponse)); - savedObjectsClientStub.update = jest.fn().mockReturnValue(Promise.resolve(mockDocResponse)); - - savedObjectsClientStub.find = jest - .fn() - .mockReturnValue(Promise.resolve({ savedObjects: [], total: 0 })); - - savedObjectsClientStub.bulkGet = jest - .fn() - .mockReturnValue(Promise.resolve({ savedObjects: [mockDocResponse] })); - } - - function stubSavedObjectsClientCreate( - resp: SimpleSavedObject | string, - resolve = true - ) { - savedObjectsClientStub.create = jest - .fn() - .mockReturnValue(resolve ? Promise.resolve(resp) : Promise.reject(resp)); - } - - /** - * Creates a new saved object with the given configuration and initializes it. - * Returns the promise that will be completed when the initialization finishes. - * - * @param {Object} config - * @returns {Promise} A promise that resolves with an instance of - * SavedObject - */ - function createInitializedSavedObject(config: SavedObjectConfig = { type: 'dashboard' }) { - const savedObject = new SavedObjectClass(config); - savedObject.title = 'my saved object'; - - return savedObject.init!(); - } - - const initSavedObjectClass = () => { - SavedObjectClass = createSavedObjectClass( - { - savedObjectsClient: savedObjectsClientStub, - indexPatterns: dataStartMock.indexPatterns, - search: { - ...dataStartMock.search, - searchSource: { - ...dataStartMock.search.searchSource, - create: createSearchSourceMock, - createEmpty: createSearchSourceMock, - }, - }, - } as unknown as SavedObjectKibanaServices, - startServices, - decoratorRegistry - ); - }; - - beforeEach(() => { - decoratorRegistry = savedObjectsDecoratorRegistryMock.create(); - initSavedObjectClass(); - }); - - describe('decorators', () => { - it('calls the decorators during construct', () => { - const decorA = { - getId: () => 'A', - decorateConfig: jest.fn(), - decorateObject: jest.fn(), - }; - const decorB = { - getId: () => 'B', - decorateConfig: jest.fn(), - decorateObject: jest.fn(), - }; - - decoratorRegistry.getOrderedDecorators.mockReturnValue([decorA, decorB]); - - initSavedObjectClass(); - createInitializedSavedObject(); - - expect(decorA.decorateConfig).toHaveBeenCalledTimes(1); - expect(decorA.decorateObject).toHaveBeenCalledTimes(1); - }); - - it('calls the decorators in correct order', () => { - const decorA = { - getId: () => 'A', - decorateConfig: jest.fn(), - decorateObject: jest.fn(), - }; - const decorB = { - getId: () => 'B', - decorateConfig: jest.fn(), - decorateObject: jest.fn(), - }; - - decoratorRegistry.getOrderedDecorators.mockReturnValue([decorA, decorB]); - - initSavedObjectClass(); - createInitializedSavedObject(); - - expect(decorA.decorateConfig.mock.invocationCallOrder[0]).toBeLessThan( - decorB.decorateConfig.mock.invocationCallOrder[0] - ); - expect(decorA.decorateObject.mock.invocationCallOrder[0]).toBeLessThan( - decorB.decorateObject.mock.invocationCallOrder[0] - ); - }); - - it('passes the mutated config and object down the decorator chain', () => { - expect.assertions(2); - - const newMappingValue = 'string'; - const newObjectMethod = jest.fn(); - - const decorA: SavedObjectDecorator = { - getId: () => 'A', - decorateConfig: (config) => { - config.mapping = { - ...config.mapping, - addedFromA: newMappingValue, - }; - }, - decorateObject: (object) => { - (object as any).newMethod = newObjectMethod; - }, - }; - const decorB: SavedObjectDecorator = { - getId: () => 'B', - decorateConfig: (config) => { - expect(config.mapping!.addedFromA).toBe(newMappingValue); - }, - decorateObject: (object) => { - expect((object as any).newMethod).toBe(newObjectMethod); - }, - }; - - decoratorRegistry.getOrderedDecorators.mockReturnValue([decorA, decorB]); - - initSavedObjectClass(); - createInitializedSavedObject(); - }); - }); - - describe('save', () => { - describe('with confirmOverwrite', () => { - it('when false does not request overwrite', () => { - stubESResponse(getMockedDocResponse('myId')); - - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then( - (savedObject) => { - stubSavedObjectsClientCreate({ - id: 'myId', - } as SimpleSavedObject); - - return savedObject.save({ confirmOverwrite: false }).then(() => { - expect(startMock.overlays.openModal).not.toHaveBeenCalled(); - }); - } - ); - }); - }); - - describe('with copyOnSave', () => { - it('as true creates a copy on save success', () => { - stubESResponse(getMockedDocResponse('myId')); - - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then( - (savedObject) => { - stubSavedObjectsClientCreate({ - type: 'dashboard', - id: 'newUniqueId', - } as SimpleSavedObject); - savedObject.copyOnSave = true; - - return savedObject.save(saveOptionsMock).then((id) => { - expect(id).toBe('newUniqueId'); - }); - } - ); - }); - - it('as true does not create a copy when save fails', () => { - const originalId = 'id1'; - stubESResponse(getMockedDocResponse(originalId)); - - return createInitializedSavedObject({ type: 'dashboard', id: originalId }).then( - (savedObject) => { - stubSavedObjectsClientCreate('simulated error', false); - savedObject.copyOnSave = true; - - return savedObject - .save(saveOptionsMock) - .then(() => { - expect(false).toBe(true); - }) - .catch(() => { - expect(savedObject.id).toBe(originalId); - }); - } - ); - }); - - it('as false does not create a copy', () => { - const myId = 'myId'; - stubESResponse(getMockedDocResponse(myId)); - - return createInitializedSavedObject({ type: 'dashboard', id: myId }).then((savedObject) => { - savedObjectsClientStub.create = jest.fn().mockImplementation(() => { - expect(savedObject.id).toBe(myId); - return Promise.resolve({ id: myId }); - }); - savedObject.copyOnSave = false; - - return savedObject.save(saveOptionsMock).then((id) => { - expect(id).toBe(myId); - }); - }); - }); - }); - - it('returns id from server on success', () => { - return createInitializedSavedObject({ type: 'dashboard' }).then((savedObject) => { - stubESResponse(getMockedDocResponse('myId')); - stubSavedObjectsClientCreate({ - type: 'dashboard', - id: 'myId', - _version: 'foo', - } as SimpleSavedObject); - - return savedObject.save(saveOptionsMock).then((id) => { - expect(id).toBe('myId'); - }); - }); - }); - - describe('updates isSaving variable', () => { - it('on success', () => { - const id = 'id'; - stubESResponse(getMockedDocResponse(id)); - - return createInitializedSavedObject({ type: 'dashboard', id }).then((savedObject) => { - savedObjectsClientStub.create = jest.fn().mockImplementation(() => { - expect(savedObject.isSaving).toBe(true); - return Promise.resolve({ - type: 'dashboard', - id, - _version: 'foo', - }); - }); - - expect(savedObject.isSaving).toBe(false); - return savedObject.save(saveOptionsMock).then(() => { - expect(savedObject.isSaving).toBe(false); - }); - }); - }); - - it('on failure', () => { - stubESResponse(getMockedDocResponse('id')); - return createInitializedSavedObject({ type: 'dashboard' }).then((savedObject) => { - savedObjectsClientStub.create = jest.fn().mockImplementation(() => { - expect(savedObject.isSaving).toBe(true); - return Promise.reject(''); - }); - - expect(savedObject.isSaving).toBe(false); - return savedObject.save(saveOptionsMock).catch(() => { - expect(savedObject.isSaving).toBe(false); - }); - }); - }); - }); - - describe('to extract references', () => { - it('when "extractReferences" function when passed in', async () => { - const id = '123'; - stubESResponse(getMockedDocResponse(id)); - const extractReferences: SavedObjectConfig['extractReferences'] = ({ - attributes, - references, - }) => { - references.push({ - name: 'test', - type: 'index-pattern', - id: 'my-index', - }); - return { attributes, references }; - }; - return createInitializedSavedObject({ type: 'dashboard', extractReferences }).then( - (savedObject) => { - stubSavedObjectsClientCreate({ - id, - _version: 'foo', - type: 'dashboard', - } as SimpleSavedObject); - - return savedObject.save(saveOptionsMock).then(() => { - const { references } = (savedObjectsClientStub.create as jest.Mock).mock.calls[0][2]; - expect(references).toHaveLength(1); - expect(references[0]).toEqual({ - name: 'test', - type: 'index-pattern', - id: 'my-index', - }); - }); - } - ); - }); - - it('when search source references saved object', () => { - const id = '123'; - stubESResponse(getMockedDocResponse(id)); - return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then( - (savedObject) => { - stubSavedObjectsClientCreate({ - id, - _version: '2', - type: 'dashboard', - } as SimpleSavedObject); - - const indexPattern = createStubIndexPattern({ - spec: { id: 'my-index', title: 'my-index', version: '1' }, - }); - savedObject.searchSource!.setField('index', indexPattern); - return savedObject.save(saveOptionsMock).then(() => { - const args = (savedObjectsClientStub.create as jest.Mock).mock.calls[0]; - expect(args[1]).toEqual({ - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - }), - }, - }); - - expect(args[2].references).toHaveLength(1); - expect(args[2].references[0]).toEqual({ - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'my-index', - }); - }); - } - ); - }); - - it('when index in searchSourceJSON is not found', () => { - const id = '123'; - stubESResponse(getMockedDocResponse(id)); - return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then( - (savedObject) => { - stubSavedObjectsClientCreate({ - id, - _version: '2', - type: 'dashboard', - } as SimpleSavedObject); - - const indexPattern = createStubIndexPattern({ - spec: { - id: 'non-existant-index', - version: '1', - }, - }); - - savedObject.searchSource!.setField('index', indexPattern); - return savedObject.save(saveOptionsMock).then(() => { - const args = (savedObjectsClientStub.create as jest.Mock).mock.calls[0]; - expect(args[1]).toEqual({ - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - }), - }, - }); - expect(args[2].references).toHaveLength(1); - expect(args[2].references[0]).toEqual({ - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'non-existant-index', - }); - }); - } - ); - }); - - it('when indexes exists in filter of searchSourceJSON', () => { - const id = '123'; - stubESResponse(getMockedDocResponse(id)); - return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then( - (savedObject) => { - stubSavedObjectsClientCreate({ - id, - _version: '2', - type: 'dashboard', - } as SimpleSavedObject); - - savedObject.searchSource!.setField('filter', [ - { - meta: { - index: 'my-index', - }, - }, - ] as any); - return savedObject.save(saveOptionsMock).then(() => { - const args = (savedObjectsClientStub.create as jest.Mock).mock.calls[0]; - expect(args[1]).toEqual({ - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - filter: [ - { - meta: { - indexRefName: - 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - }, - }, - ], - }), - }, - }); - expect(args[2].references).toHaveLength(1); - expect(args[2].references[0]).toEqual({ - name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - type: 'index-pattern', - id: 'my-index', - }); - }); - } - ); - }); - }); - }); - - describe('applyESResp', () => { - it('throws error if not found', () => { - return createInitializedSavedObject({ type: 'dashboard' }).then((savedObject) => { - const response = { _source: {} }; - try { - savedObject.applyESResp(response); - expect(true).toBe(false); - } catch (err) { - expect(!!err).toBe(true); - } - }); - }); - - it('preserves original defaults if not overridden', () => { - const id = 'anid'; - const preserveMeValue = 'here to stay!'; - const config = { - defaults: { - preserveMe: preserveMeValue, - }, - type: 'dashboard', - id, - }; - - const mockDocResponse = getMockedDocResponse(id); - stubESResponse(mockDocResponse); - - const savedObject = new SavedObjectClass(config); - return savedObject.init!() - .then(() => { - expect(savedObject._source.preserveMe).toEqual(preserveMeValue); - const response = { found: true, _source: {} }; - return savedObject.applyESResp(response); - }) - .then(() => { - expect(savedObject._source.preserveMe).toEqual(preserveMeValue); - }); - }); - - it('overrides defaults', () => { - const id = 'anid'; - const config = { - defaults: { - flower: 'rose', - }, - type: 'dashboard', - id, - }; - - stubESResponse(getMockedDocResponse(id)); - - const savedObject = new SavedObjectClass(config); - return savedObject.init!() - .then(() => { - expect(savedObject._source.flower).toEqual('rose'); - const response = { - found: true, - _source: { - flower: 'orchid', - }, - }; - return savedObject.applyESResp(response); - }) - .then(() => { - expect(savedObject._source.flower).toEqual('orchid'); - }); - }); - - it('overrides previous _source and default values', () => { - const id = 'anid'; - const config = { - defaults: { - dinosaurs: { - tRex: 'is the scariest', - }, - }, - type: 'dashboard', - id, - }; - - const mockDocResponse = getMockedDocResponse(id, { - attributes: { dinosaurs: { tRex: 'is not so bad' } }, - }); - stubESResponse(mockDocResponse); - - const savedObject = new SavedObjectClass(config); - return savedObject.init!() - .then(() => { - const response = { - found: true, - _source: { dinosaurs: { tRex: 'has big teeth' } }, - }; - - return savedObject.applyESResp(response); - }) - .then(() => { - expect((savedObject._source as any).dinosaurs.tRex).toEqual('has big teeth'); - }); - }); - - it('does not inject references when references array is missing', async () => { - const injectReferences = jest.fn(); - const config = { - type: 'dashboard', - injectReferences, - }; - const savedObject = new SavedObjectClass(config); - return savedObject.init!() - .then(() => { - const response = { - found: true, - _source: { - dinosaurs: { tRex: 'has big teeth' }, - }, - }; - return savedObject.applyESResp(response); - }) - .then(() => { - expect(injectReferences).not.toHaveBeenCalled(); - }); - }); - - it('does not inject references when references array is empty', async () => { - const injectReferences = jest.fn(); - const config = { - type: 'dashboard', - injectReferences, - }; - const savedObject = new SavedObjectClass(config); - return savedObject.init!() - .then(() => { - const response = { - found: true, - _source: { - dinosaurs: { tRex: 'has big teeth' }, - }, - references: [], - }; - return savedObject.applyESResp(response); - }) - .then(() => { - expect(injectReferences).not.toHaveBeenCalled(); - }); - }); - - it('injects references when function is provided and references exist', async () => { - const injectReferences = jest.fn(); - const config = { - type: 'dashboard', - injectReferences, - }; - const savedObject = new SavedObjectClass(config); - return savedObject.init!() - .then(() => { - const response = { - found: true, - _source: { - dinosaurs: { tRex: 'has big teeth' }, - }, - references: [{}], - }; - return savedObject.applyESResp(response); - }) - .then(() => { - expect(injectReferences).toHaveBeenCalledTimes(1); - }); - }); - - it('passes references to search source parsing function', async () => { - SavedObjectClass = createSavedObjectClass( - { - savedObjectsClient: savedObjectsClientStub, - indexPatterns: dataStartMock.indexPatterns, - search: { - ...dataStartMock.search, - }, - } as unknown as SavedObjectKibanaServices, - startServices, - decoratorRegistry - ); - const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true }); - return savedObject.init!().then(async () => { - const searchSourceJSON = JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - filter: [ - { - meta: { - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - }, - }, - ], - }); - const response = { - found: true, - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON, - }, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'my-index-1', - }, - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - type: 'index-pattern', - id: 'my-index-2', - }, - ], - }; - await savedObject.applyESResp(response); - expect(dataStartMock.search.searchSource.create).toBeCalledWith({ - filter: [{ meta: { index: 'my-index-2' } }], - index: 'my-index-1', - }); - }); - }); - }); - - describe('config', () => { - it('afterESResp is called', () => { - const afterESRespCallback = jest.fn(); - const config = { - type: 'dashboard', - afterESResp: afterESRespCallback, - }; - - return createInitializedSavedObject(config).then(() => { - expect(afterESRespCallback).toHaveBeenCalled(); - }); - }); - - describe('searchSource', () => { - it('when true, creates index', () => { - const indexPatternId = 'testIndexPattern'; - const afterESRespCallback = jest.fn(); - - const config: SavedObjectConfig = { - type: 'dashboard', - afterESResp: afterESRespCallback, - searchSource: true, - indexPattern: { id: indexPatternId } as DataView, - }; - - stubESResponse( - getMockedDocResponse(indexPatternId, { - attributes: { - title: 'testIndexPattern', - }, - }) - ); - - const savedObject = new SavedObjectClass(config); - savedObject.hydrateIndexPattern = jest.fn().mockImplementation(() => { - const indexPattern = createStubIndexPattern({ - spec: { - id: indexPatternId, - title: indexPatternId, - }, - }); - savedObject.searchSource!.setField('index', indexPattern); - return Promise.resolve(indexPattern); - }); - expect(!!savedObject.searchSource!.getField('index')).toBe(false); - - return savedObject.init!().then(() => { - expect(afterESRespCallback).toHaveBeenCalled(); - const index = savedObject.searchSource!.getField('index'); - expect(index instanceof DataView).toBe(true); - expect(index!.id).toEqual(indexPatternId); - }); - }); - - it('when false, does not create index', () => { - const indexPatternId = 'testIndexPattern'; - const afterESRespCallback = jest.fn(); - - const config: SavedObjectConfig = { - type: 'dashboard', - afterESResp: afterESRespCallback, - searchSource: false, - indexPattern: { id: indexPatternId } as DataView, - }; - - stubESResponse(getMockedDocResponse(indexPatternId)); - - const savedObject = new SavedObjectClass(config); - expect(!!savedObject.searchSource).toBe(false); - - return savedObject.init!().then(() => { - expect(afterESRespCallback).toHaveBeenCalled(); - expect(!!savedObject.searchSource).toBe(false); - }); - }); - }); - - describe('type', () => { - it('that is not specified throws an error', (done) => { - const config = {}; - - const savedObject = new SavedObjectClass(config); - savedObject.init!().catch(() => { - done(); - }); - }); - - it('that is invalid invalid throws an error', () => { - const config = { type: 'notypeexists' }; - - const savedObject = new SavedObjectClass(config); - try { - savedObject.init!(); - expect(false).toBe(true); - } catch (err) { - expect(err).not.toBeNull(); - } - }); - - it('that is valid passes', () => { - const config = { type: 'dashboard' }; - return new SavedObjectClass(config).init!(); - }); - }); - - describe('defaults', () => { - function getTestDefaultConfig(extraOptions: object = {}) { - return { - defaults: { testDefault: 'hi' }, - type: 'dashboard', - ...extraOptions, - }; - } - - function expectDefaultApplied(config: SavedObjectConfig) { - return createInitializedSavedObject(config).then((savedObject) => { - expect(savedObject.defaults).toBe(config.defaults); - }); - } - - describe('applied to object when id', () => { - it('is not specified', () => { - expectDefaultApplied(getTestDefaultConfig()); - }); - - it('is undefined', () => { - expectDefaultApplied(getTestDefaultConfig({ id: undefined })); - }); - - it('is 0', () => { - expectDefaultApplied(getTestDefaultConfig({ id: 0 })); - }); - - it('is false', () => { - expectDefaultApplied(getTestDefaultConfig({ id: false })); - }); - }); - - it('applied to source if an id is given', () => { - const myId = 'myid'; - const customDefault = 'hi'; - const initialOverwriteMeValue = 'this should get overwritten by the server response'; - - const config = { - defaults: { - overwriteMe: initialOverwriteMeValue, - customDefault, - }, - type: 'dashboard', - id: myId, - }; - - const serverValue = 'this should override the initial default value given'; - - const mockDocResponse = getMockedDocResponse(myId, { - attributes: { overwriteMe: serverValue }, - }); - - stubESResponse(mockDocResponse); - - return createInitializedSavedObject(config).then((savedObject) => { - expect(!!savedObject._source).toBe(true); - expect(savedObject.defaults).toBe(config.defaults); - expect(savedObject._source.overwriteMe).toBe(serverValue); - expect(savedObject._source.customDefault).toBe(customDefault); - }); - }); - }); - }); -}); diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.ts b/src/plugins/saved_objects/public/saved_object/saved_object.ts deleted file mode 100644 index cb83021582b2e..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/saved_object.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -/** - * @name SavedObject - * - * NOTE: SavedObject seems to track a reference to an object in ES, - * and surface methods for CRUD functionality (save and delete). This seems - * similar to how Backbone Models work. - * - * This class seems to interface with ES primarily through the es Angular - * service and the saved object api. - */ -import { SavedObject, SavedObjectConfig, SavedObjectKibanaServices, StartServices } from '../types'; -import { ISavedObjectDecoratorRegistry } from './decorators'; -import { buildSavedObject } from './helpers/build_saved_object'; - -export function createSavedObjectClass( - services: SavedObjectKibanaServices, - startServices: StartServices, - decoratorRegistry: ISavedObjectDecoratorRegistry -) { - /** - * The SavedObject class is a base class for saved objects loaded from the server and - * provides additional functionality besides loading/saving/deleting/etc. - * - * It is overloaded and configured to provide type-aware functionality. - * @param {*} config - */ - class SavedObjectClass { - constructor(config: SavedObjectConfig = {}) { - // @ts-ignore - const self: SavedObject = this; - buildSavedObject( - self, - config, - services, - startServices, - decoratorRegistry.getOrderedDecorators(services) - ); - } - } - - return SavedObjectClass as new (config: SavedObjectConfig) => SavedObject; -} diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index 4dbced6ac31a7..ccec7b43ad9f1 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -11,7 +11,6 @@ "@kbn/i18n", "@kbn/data-views-plugin", "@kbn/i18n-react", - "@kbn/utility-types", "@kbn/ui-theme", "@kbn/react-kibana-mount", "@kbn/test-jest-helpers", diff --git a/src/plugins/saved_objects_tagging_oss/public/api.mock.ts b/src/plugins/saved_objects_tagging_oss/public/api.mock.ts index 09655c6d28108..07539956c1485 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.mock.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.mock.ts @@ -55,8 +55,6 @@ type SavedObjectsTaggingApiUiMock = Omit, const createApiUiMock = () => { const mock: SavedObjectsTaggingApiUiMock = { components: createApiUiComponentsMock(), - // TS is very picky with type guards - hasTagDecoration: jest.fn() as any, getSearchBarFilter: jest.fn(), getTableColumnDefinition: jest.fn(), convertNameToReference: jest.fn(), diff --git a/src/plugins/saved_objects_tagging_oss/public/api.ts b/src/plugins/saved_objects_tagging_oss/public/api.ts index f211250532094..fa7f3665ac023 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.ts @@ -12,8 +12,6 @@ import { SearchFilterConfig, EuiTableFieldDataColumnType, EuiComboBoxProps } fro import type { FunctionComponent } from 'react'; import { SavedObject, SavedObjectReference } from '@kbn/core/types'; import { SavedObjectsFindOptionsReference } from '@kbn/core/public'; -import { SavedObject as SavedObjectClass } from '@kbn/saved-objects-plugin/public'; -import { TagDecoratedSavedObject } from './decorator'; import { ITagsClient, Tag, TagWithOptionalId } from '../common'; /** @@ -50,11 +48,6 @@ export interface ITagsCache { getState$(params?: { waitForInitialization?: boolean }): Observable; } -/** - * @public - */ -export type SavedObjectTagDecoratorTypeGuard = SavedObjectsTaggingApiUi['hasTagDecoration']; - /** * React components and utility methods to use the SO tagging feature * @@ -72,13 +65,6 @@ export interface SavedObjectsTaggingApiUi { */ getTagList(): Tag[]; - /** - * Type-guard to safely manipulate tag-enhanced `SavedObject` from the `savedObject` plugin. - * - * @param object - */ - hasTagDecoration(object: SavedObjectClass): object is TagDecoratedSavedObject; - /** * Return a filter that can be used to filter by tag with `EuiSearchBar` or EUI tables using `EuiSearchBar`. * diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.test.mocks.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.test.mocks.ts deleted file mode 100644 index 5ca655b8a667d..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.test.mocks.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export const injectTagReferencesMock = jest.fn(); -jest.doMock('./inject_tag_references', () => ({ - injectTagReferences: injectTagReferencesMock, -})); - -export const extractTagReferencesMock = jest.fn(); -jest.doMock('./extract_tag_references', () => ({ - extractTagReferences: extractTagReferencesMock, -})); diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.test.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.test.ts deleted file mode 100644 index 37536804e6c87..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { extractTagReferencesMock, injectTagReferencesMock } from './decorate_config.test.mocks'; - -import { SavedObjectConfig } from '@kbn/saved-objects-plugin/public'; -import { decorateConfig } from './decorate_config'; - -describe('decorateConfig', () => { - afterEach(() => { - extractTagReferencesMock.mockReset(); - injectTagReferencesMock.mockReset(); - }); - - describe('mapping', () => { - it('adds the `__tags` key to the config mapping', () => { - const config: SavedObjectConfig = { - mapping: { - someText: 'text', - someNum: 'number', - }, - }; - - decorateConfig(config); - - expect(config.mapping).toEqual({ - __tags: 'text', - someText: 'text', - someNum: 'number', - }); - }); - - it('adds mapping to the config if missing', () => { - const config: SavedObjectConfig = {}; - - decorateConfig(config); - - expect(config.mapping).toEqual({ - __tags: 'text', - }); - }); - }); - - describe('injectReferences', () => { - it('decorates to only call `injectTagReferences` when not present on the config', () => { - const config: SavedObjectConfig = {}; - - decorateConfig(config); - - const object: any = Symbol('object'); - const references: any = Symbol('referebces'); - - config.injectReferences!(object, references); - - expect(injectTagReferencesMock).toHaveBeenCalledTimes(1); - expect(injectTagReferencesMock).toHaveBeenCalledWith(object, references); - }); - - it('decorates to call both functions when present on the config', () => { - const initialInjectReferences = jest.fn(); - - const config: SavedObjectConfig = { - injectReferences: initialInjectReferences, - }; - - decorateConfig(config); - - const object: any = Symbol('object'); - const references: any = Symbol('references'); - - config.injectReferences!(object, references); - - expect(initialInjectReferences).toHaveBeenCalledTimes(1); - expect(initialInjectReferences).toHaveBeenCalledWith(object, references); - - expect(injectTagReferencesMock).toHaveBeenCalledTimes(1); - expect(injectTagReferencesMock).toHaveBeenCalledWith(object, references); - }); - }); - - describe('extractReferences', () => { - it('decorates to only call `extractTagReference` when not present on the config', () => { - const config: SavedObjectConfig = {}; - - decorateConfig(config); - - const params: any = Symbol('params'); - const expectedReturn = Symbol('return-from-extractTagReferences'); - - extractTagReferencesMock.mockReturnValue(expectedReturn); - - const result = config.extractReferences!(params); - - expect(result).toBe(expectedReturn); - - expect(extractTagReferencesMock).toHaveBeenCalledTimes(1); - expect(extractTagReferencesMock).toHaveBeenCalledWith(params); - }); - - it('decorates to call both functions in order when present on the config', () => { - const initialExtractReferences = jest.fn(); - - const config: SavedObjectConfig = { - extractReferences: initialExtractReferences, - }; - - decorateConfig(config); - - const params: any = Symbol('initial-params'); - const initialReturn = Symbol('return-from-initial-extractReferences'); - const tagReturn = Symbol('return-from-extractTagReferences'); - - initialExtractReferences.mockReturnValue(initialReturn); - extractTagReferencesMock.mockReturnValue(tagReturn); - - const result = config.extractReferences!(params); - - expect(initialExtractReferences).toHaveBeenCalledTimes(1); - expect(initialExtractReferences).toHaveBeenCalledWith(params); - - expect(extractTagReferencesMock).toHaveBeenCalledTimes(1); - expect(extractTagReferencesMock).toHaveBeenCalledWith(initialReturn); - - expect(result).toBe(tagReturn); - }); - }); -}); diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.ts deleted file mode 100644 index 8cdb6a4229d44..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_config.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectConfig } from '@kbn/saved-objects-plugin/public'; -import { injectTagReferences } from './inject_tag_references'; -import { extractTagReferences } from './extract_tag_references'; - -export const decorateConfig = (config: SavedObjectConfig) => { - config.mapping = { - ...config.mapping, - __tags: 'text', - }; - - const initialExtractReferences = config.extractReferences; - const initialInjectReferences = config.injectReferences; - - config.injectReferences = (object, references) => { - if (initialInjectReferences) { - initialInjectReferences(object, references); - } - injectTagReferences(object, references); - }; - - config.extractReferences = (attrsAndRefs) => { - if (initialExtractReferences) { - attrsAndRefs = initialExtractReferences(attrsAndRefs); - } - return extractTagReferences(attrsAndRefs); - }; -}; diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_object.test.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_object.test.ts deleted file mode 100644 index a997e7c2591a3..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_object.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { InternalTagDecoratedSavedObject } from './types'; -import { decorateObject } from './decorate_object'; - -const createObject = (): InternalTagDecoratedSavedObject => { - // we really just need TS not to complain here. - return {} as InternalTagDecoratedSavedObject; -}; - -describe('decorateObject', () => { - it('adds the `getTags` method', () => { - const object = createObject(); - object.__tags = ['foo', 'bar']; - - decorateObject(object); - - expect(object.getTags).toBeDefined(); - expect(object.getTags()).toEqual(['foo', 'bar']); - }); - - it('adds the `setTags` method', () => { - const object = createObject(); - object.__tags = ['foo', 'bar']; - - decorateObject(object); - - expect(object.setTags).toBeDefined(); - - object.setTags(['hello', 'dolly']); - - expect(object.getTags()).toEqual(['hello', 'dolly']); - }); -}); diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_object.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_object.ts deleted file mode 100644 index 0c03863b55ea9..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/decorate_object.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { InternalTagDecoratedSavedObject } from './types'; - -/** - * Enhance the object with tag accessors - */ -export const decorateObject = (object: InternalTagDecoratedSavedObject) => { - object.getTags = () => { - return object.__tags ?? []; - }; - object.setTags = (tagIds) => { - object.__tags = tagIds; - }; -}; diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/extract_tag_references.test.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/extract_tag_references.test.ts deleted file mode 100644 index 2ac4c4d0db8c2..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/extract_tag_references.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectReference } from '@kbn/core/public'; -import { extractTagReferences } from './extract_tag_references'; - -const ref = (type: string, id: string, name = `ref-to-${type}-${id}`): SavedObjectReference => ({ - id, - type, - name, -}); - -const tagRef = (id: string): SavedObjectReference => ref('tag', id, `tag-${id}`); - -describe('extractTagReferences', () => { - it('generate tag references from the attributes', () => { - const attributes = { - __tags: ['tag-id-1', 'tag-id-2'], - }; - const references: SavedObjectReference[] = []; - - const { references: resultRefs } = extractTagReferences({ - attributes, - references, - }); - - expect(resultRefs).toEqual([tagRef('tag-id-1'), tagRef('tag-id-2')]); - }); - - it('removes the `__tag` property from the attributes', () => { - const attributes = { - someString: 'foo', - someNumber: 42, - __tags: ['tag-id-1', 'tag-id-2'], - }; - const references: SavedObjectReference[] = []; - - const { attributes: resultAttrs } = extractTagReferences({ - attributes, - references, - }); - - expect(resultAttrs).toEqual({ someString: 'foo', someNumber: 42 }); - }); - - it('preserves the other references', () => { - const attributes = { - __tags: ['tag-id-1'], - }; - - const refA = ref('dashboard', 'dash-1'); - const refB = ref('visualization', 'vis-1'); - - const { references: resultRefs } = extractTagReferences({ - attributes, - references: [refA, refB], - }); - - expect(resultRefs).toEqual([refA, refB, tagRef('tag-id-1')]); - }); - - it('does not fail if `attributes` does not contain `__tags`', () => { - const attributes = { - someString: 'foo', - someNumber: 42, - }; - - const refA = ref('dashboard', 'dash-1'); - const refB = ref('visualization', 'vis-1'); - - const { attributes: resultAttrs, references: resultRefs } = extractTagReferences({ - attributes, - references: [refA, refB], - }); - - expect(resultRefs).toEqual([refA, refB]); - expect(resultAttrs).toEqual({ someString: 'foo', someNumber: 42 }); - }); - - it('removes duplicated tags', () => { - const attributes = { - __tags: ['tag-id-1', 'tag-id-1', 'tag-id-1', 'tag-id-1', 'tag-id-2'], - }; - - const { references: resultRefs } = extractTagReferences({ - attributes, - references: [] as SavedObjectReference[], - }); - - expect(resultRefs).toEqual([ - { id: 'tag-id-1', name: 'tag-tag-id-1', type: 'tag' }, - { id: 'tag-id-2', name: 'tag-tag-id-2', type: 'tag' }, - ]); - }); -}); diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/extract_tag_references.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/extract_tag_references.ts deleted file mode 100644 index 35ee2a75a8803..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/extract_tag_references.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectConfig } from '@kbn/saved-objects-plugin/public'; - -/** - * Extract the tag references from the object's attribute - * - * (`extractReferences` is used when persisting the saved object to the backend) - */ -export const extractTagReferences: Required['extractReferences'] = ({ - attributes, - references, -}) => { - const { __tags, ...otherAttributes } = attributes; - const tags = [...new Set(__tags as string[])] ?? []; - return { - attributes: otherAttributes, - references: [ - ...references, - ...tags.map((tagId) => ({ - id: tagId, - type: 'tag', - name: `tag-${tagId}`, - })), - ], - }; -}; diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/factory.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/factory.ts deleted file mode 100644 index 003fa5c626b09..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectDecoratorFactory } from '@kbn/saved-objects-plugin/public'; -import { InternalTagDecoratedSavedObject } from './types'; -import { decorateConfig } from './decorate_config'; -import { decorateObject } from './decorate_object'; - -export const decoratorId = 'tag'; - -export const tagDecoratorFactory: SavedObjectDecoratorFactory< - InternalTagDecoratedSavedObject -> = () => { - return { - getId: () => decoratorId, - decorateConfig, - decorateObject, - }; -}; diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/inject_tag_references.test.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/inject_tag_references.test.ts deleted file mode 100644 index 2eb3db3937b86..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/inject_tag_references.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectReference } from '@kbn/core/public'; -import { injectTagReferences } from './inject_tag_references'; -import { InternalTagDecoratedSavedObject } from './types'; - -const ref = (type: string, id: string): SavedObjectReference => ({ - id, - type, - name: `ref-to-${type}-${id}`, -}); - -const tagRef = (id: string) => ref('tag', id); - -const createObject = (): InternalTagDecoratedSavedObject => { - // we really just need TS not to complain here. - return {} as InternalTagDecoratedSavedObject; -}; - -describe('injectTagReferences', () => { - let object: InternalTagDecoratedSavedObject; - - beforeEach(() => { - object = createObject(); - }); - - it('injects the `tag` references to the `__tags` field', () => { - const references = [tagRef('tag-id-1'), tagRef('tag-id-2')]; - - injectTagReferences(object, references); - - expect(object.__tags).toEqual(['tag-id-1', 'tag-id-2']); - }); - - it('only process the tag references', () => { - const references = [ - tagRef('tag-id-1'), - ref('dashboard', 'foo'), - tagRef('tag-id-2'), - ref('dashboard', 'baz'), - ]; - - injectTagReferences(object, references); - - expect(object.__tags).toEqual(['tag-id-1', 'tag-id-2']); - }); - - it('injects an empty list when not tag references are present', () => { - injectTagReferences(object, [ref('dashboard', 'foo'), ref('dashboard', 'baz')]); - - expect(object.__tags).toEqual([]); - }); -}); diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/inject_tag_references.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/inject_tag_references.ts deleted file mode 100644 index fd550a81837f9..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/inject_tag_references.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectConfig } from '@kbn/saved-objects-plugin/public'; -import { InternalTagDecoratedSavedObject } from './types'; - -/** - * Inject the tags back into the object's references - * - * (`injectReferences`) is used when fetching the object from the backend - */ -export const injectTagReferences: Required['injectReferences'] = ( - object, - references = [] -) => { - (object as unknown as InternalTagDecoratedSavedObject).__tags = references - .filter(({ type }) => type === 'tag') - .map(({ id }) => id); -}; diff --git a/src/plugins/saved_objects_tagging_oss/public/decorator/types.ts b/src/plugins/saved_objects_tagging_oss/public/decorator/types.ts deleted file mode 100644 index b50f1b183ae39..0000000000000 --- a/src/plugins/saved_objects_tagging_oss/public/decorator/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObject } from '@kbn/saved-objects-plugin/public'; - -/** - * @public - */ -export type TagDecoratedSavedObject = SavedObject & { - getTags(): string[]; - setTags(tags: string[]): void; -}; - -/** - * @internal - */ -export type InternalTagDecoratedSavedObject = TagDecoratedSavedObject & { - __tags: string[]; -}; diff --git a/src/plugins/saved_objects_tagging_oss/public/index.ts b/src/plugins/saved_objects_tagging_oss/public/index.ts index 58dce1bd20842..b754d763427c8 100644 --- a/src/plugins/saved_objects_tagging_oss/public/index.ts +++ b/src/plugins/saved_objects_tagging_oss/public/index.ts @@ -23,11 +23,8 @@ export type { ParsedSearchQuery, ParseSearchQueryOptions, SavedObjectSaveModalTagSelectorComponentProps, - SavedObjectTagDecoratorTypeGuard, GetTableColumnDefinitionOptions, } from './api'; -export type { TagDecoratedSavedObject } from './decorator'; - export const plugin = (initializerContext: PluginInitializerContext) => new SavedObjectTaggingOssPlugin(initializerContext); diff --git a/src/plugins/saved_objects_tagging_oss/public/plugin.test.ts b/src/plugins/saved_objects_tagging_oss/public/plugin.test.ts index 7c26f5a9a8289..bb3f0f0e78f83 100644 --- a/src/plugins/saved_objects_tagging_oss/public/plugin.test.ts +++ b/src/plugins/saved_objects_tagging_oss/public/plugin.test.ts @@ -8,8 +8,6 @@ */ import { coreMock } from '@kbn/core/public/mocks'; -import { savedObjectsPluginMock } from '@kbn/saved-objects-plugin/public/mocks'; -import { tagDecoratorConfig } from './decorator'; import { taggingApiMock } from './api.mock'; import { SavedObjectTaggingOssPlugin } from './plugin'; @@ -22,23 +20,6 @@ describe('SavedObjectTaggingOssPlugin', () => { plugin = new SavedObjectTaggingOssPlugin(coreMock.createPluginInitializerContext()); }); - describe('#setup', () => { - it('registers the tag SO decorator if the `savedObjects` plugin is present', () => { - const savedObjects = savedObjectsPluginMock.createSetupContract(); - - plugin.setup(coreSetup, { savedObjects }); - - expect(savedObjects.registerDecorator).toHaveBeenCalledTimes(1); - expect(savedObjects.registerDecorator).toHaveBeenCalledWith(tagDecoratorConfig); - }); - - it('does not fail if the `savedObjects` plugin is not present', () => { - expect(() => { - plugin.setup(coreSetup, {}); - }).not.toThrow(); - }); - }); - describe('#start', () => { let coreStart: ReturnType; @@ -54,7 +35,7 @@ describe('SavedObjectTaggingOssPlugin', () => { it('returns the tagging API if registered', async () => { const taggingApi = taggingApiMock.create(); - const { registerTaggingApi } = plugin.setup(coreSetup, {}); + const { registerTaggingApi } = plugin.setup(coreSetup); registerTaggingApi(Promise.resolve(taggingApi)); @@ -66,7 +47,7 @@ describe('SavedObjectTaggingOssPlugin', () => { expect(getTaggingApi()).toStrictEqual(taggingApi); }); it('does not return the tagging API if not registered', async () => { - plugin.setup(coreSetup, {}); + plugin.setup(coreSetup); await nextTick(); @@ -76,7 +57,7 @@ describe('SavedObjectTaggingOssPlugin', () => { expect(getTaggingApi()).toBeUndefined(); }); it('does not return the tagging API if resolution promise rejects', async () => { - const { registerTaggingApi } = plugin.setup(coreSetup, {}); + const { registerTaggingApi } = plugin.setup(coreSetup); registerTaggingApi(Promise.reject(new Error('something went bad'))); diff --git a/src/plugins/saved_objects_tagging_oss/public/plugin.ts b/src/plugins/saved_objects_tagging_oss/public/plugin.ts index 94a77dcdf2610..c6097b447ade7 100644 --- a/src/plugins/saved_objects_tagging_oss/public/plugin.ts +++ b/src/plugins/saved_objects_tagging_oss/public/plugin.ts @@ -8,29 +8,18 @@ */ import { CoreSetup, CoreStart, PluginInitializerContext, Plugin } from '@kbn/core/public'; -import { SavedObjectSetup } from '@kbn/saved-objects-plugin/public'; import { SavedObjectTaggingOssPluginSetup, SavedObjectTaggingOssPluginStart } from './types'; import { SavedObjectsTaggingApi } from './api'; -import { tagDecoratorConfig } from './decorator'; - -interface SetupDeps { - savedObjects?: SavedObjectSetup; -} export class SavedObjectTaggingOssPlugin - implements - Plugin + implements Plugin { private apiRegistered = false; private api?: SavedObjectsTaggingApi; constructor(context: PluginInitializerContext) {} - public setup({}: CoreSetup, { savedObjects }: SetupDeps) { - if (savedObjects) { - savedObjects.registerDecorator(tagDecoratorConfig); - } - + public setup({}: CoreSetup) { return { registerTaggingApi: (provider: Promise) => { if (this.apiRegistered) { diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json index 6b98cba4cbd12..fa0436bab9161 100644 --- a/src/plugins/saved_objects_tagging_oss/tsconfig.json +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -9,7 +9,6 @@ ], "kbn_references": [ "@kbn/core", - "@kbn/saved-objects-plugin", ], "exclude": [ "target/**/*", diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx index ac1295d9a2bb4..fcebe456bd8c5 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -14,7 +14,7 @@ import useMount from 'react-use/lib/useMount'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggParam, IAggConfig, IFieldParamType, KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { IAggConfig, IFieldParamType, KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { formatListAsProse, parseCommaSeparatedList, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; @@ -47,7 +47,7 @@ function FieldParamEditor({ : []; const onChange = (options: EuiComboBoxOptionOption[]) => { - const selectedOption: DataViewField = get(options, '0.target'); + const selectedOption: DataViewField | undefined = get(options, '0.target'); if (!(aggParam.required && !selectedOption)) { setValue(selectedOption); } @@ -158,9 +158,8 @@ function getFieldTypesString(agg: IAggConfig) { } function getFieldTypes(agg: IAggConfig) { - const param = - get(agg, 'type.params', []).find((p: AggParam) => p.name === 'field') || - ({} as IFieldParamType); + const param = (get(agg, 'type.params', []).find((p) => p.name === 'field') || + {}) as IFieldParamType; return parseCommaSeparatedList(param.filterFieldTypes || []); } diff --git a/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx index eba2111d96d58..8d21aebf998b8 100644 --- a/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/time_interval.tsx @@ -147,7 +147,7 @@ function TimeIntervalParamEditor({ const onCustomInterval = (customValue: string) => setValue(customValue.trim()); const onChange = (opts: EuiComboBoxOptionOption[]) => { - const selectedOpt: ComboBoxOption = get(opts, '0'); + const selectedOpt = get(opts, '0'); setValue(selectedOpt ? selectedOpt.key : ''); }; diff --git a/src/plugins/vis_types/timelion/public/helpers/arg_value_suggestions.ts b/src/plugins/vis_types/timelion/public/helpers/arg_value_suggestions.ts index 049577064f44a..94fb21b061c38 100644 --- a/src/plugins/vis_types/timelion/public/helpers/arg_value_suggestions.ts +++ b/src/plugins/vis_types/timelion/public/helpers/arg_value_suggestions.ts @@ -23,7 +23,7 @@ export function getArgValueSuggestions() { // index argument not provided return; } - const indexPatternTitle = get(indexPatternArg, 'value.text'); + const indexPatternTitle = get(indexPatternArg, 'value.text', ''); return (await indexPatterns.find(indexPatternTitle, 1)).find( (index) => index.title === indexPatternTitle diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts index 6f3eb40c7360f..520064c2d42b4 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/helpers/get_splits.ts @@ -20,7 +20,7 @@ import type { Panel, Series } from '../../../../common/types'; import type { BaseMeta } from '../request_processors/types'; const getTimeSeries = (resp: TRawResponse, series: Series) => - get(resp, `aggregations.timeseries`) || get(resp, `aggregations.${series.id}.timeseries`); + get(resp, `aggregations.timeseries`) || get(resp, [`aggregations`, series.id, `timeseries`]); interface SplittedData { id: string; @@ -49,7 +49,7 @@ export async function getSplits>> { if (!meta) { - meta = get(resp, `aggregations.${series.id}.meta`); + meta = get(resp, `aggregations.${series.id}.meta`) as TMeta | undefined; } const color = new Color(series.color); @@ -81,7 +81,7 @@ export async function getSplits { - const bucket = get(resp, `aggregations.${series.id}.buckets.${filter.id}`); + const bucket = get(resp, [`aggregations`, series.id, `buckets`, filter.id!]); // using array path because the dotted string failed to resolve the types bucket.id = `${series.id}${SERIES_SEPARATOR}${filter.id}`; bucket.key = filter.id; bucket.splitByLabel = splitByLabel; diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts index 44c520fc766c1..d2300ca6d05a9 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/normalize_query.ts @@ -26,9 +26,7 @@ const hasSiblingPipelineAggregation = (aggs: Record = {}) => */ export const normalizeQuery: TableRequestProcessorsFunction = () => { return () => (doc) => { - const series = get(doc, 'aggs.pivot.aggs') as Array<{ - aggs: Record; - }>; + const series = get(doc, 'aggs.pivot.aggs'); const normalizedSeries = {}; forEach(series, (value, seriesId) => { diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx index 2ec89ec45d7d7..c2cede542d979 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx @@ -109,8 +109,8 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) { } if (lastCustomLabels[axis.id] !== newCustomLabel && newCustomLabel !== '') { - const lastSeriesAggType = get(lastSeriesAgg, `${matchingSeries[0].id}.type`); - const lastSeriesAggField = get(lastSeriesAgg, `${matchingSeries[0].id}.field`); + const lastSeriesAggType = get(lastSeriesAgg, [matchingSeries[0].id, `type`]); // using array path vs. string because type inference was broken + const lastSeriesAggField = get(lastSeriesAgg, [matchingSeries[0].id, `field`]); const matchingSeriesAggType = get(matchingSeries, '[0]type.name', ''); const matchingSeriesAggField = get(matchingSeries, '[0]params.field.name', ''); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index e779282fa147a..52c76a426dc14 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -405,7 +405,7 @@ export const getVisualizeEmbeddableFactory: (deps: { } const currentVis = vis$.getValue(); if (!disableTriggers) { - const triggerId = get( + const triggerId: string = get( VIS_EVENT_TO_TRIGGER, event.name, VIS_EVENT_TO_TRIGGER.filter diff --git a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx index 85166441a1634..4f6bfa344a0a3 100644 --- a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx @@ -481,7 +481,11 @@ export class VisualizeEmbeddable return; } if (!this.input.disableTriggers) { - const triggerId = get(VIS_EVENT_TO_TRIGGER, event.name, VIS_EVENT_TO_TRIGGER.filter); + const triggerId: string = get( + VIS_EVENT_TO_TRIGGER, + event.name, + VIS_EVENT_TO_TRIGGER.filter + ); let context; if (triggerId === VIS_EVENT_TO_TRIGGER.applyFilter) { diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 6d58b3a65c11f..2ba40ef14d38a 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -17,7 +17,6 @@ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; -import { savedObjectsPluginMock } from '@kbn/saved-objects-plugin/public/mocks'; import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks'; import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks'; import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks'; @@ -77,7 +76,6 @@ const createInstance = async () => { embeddable: embeddablePluginMock.createStartContract(), spaces: spacesPluginMock.createStartContract(), savedObjectsClient: coreMock.createStart().savedObjects.client, - savedObjects: savedObjectsPluginMock.createStartContract(), savedObjectsTaggingOss: savedObjectTaggingOssPluginMock.createStart(), savedSearch: savedSearchPluginMock.createStartContract(), navigation: navigationPluginMock.createStartContract(), diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 24a2c488e0f79..fcb3dc5137161 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -38,7 +38,6 @@ import type { SavedObjectsClientContract, } from '@kbn/core/public'; import { UiActionsStart, UiActionsSetup, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; -import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { Setup as InspectorSetup, @@ -166,7 +165,6 @@ export interface VisualizationsStartDeps { application: ApplicationStart; navigation: NavigationStart; presentationUtil: PresentationUtilPluginStart; - savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; savedSearch: SavedSearchPublicPluginStart; spaces?: SpacesPluginStart; diff --git a/test/api_integration/apis/kql_telemetry/kql_telemetry.ts b/test/api_integration/apis/kql_telemetry/kql_telemetry.ts index 7d3224c0306a5..5701c171b4683 100644 --- a/test/api_integration/apis/kql_telemetry/kql_telemetry.ts +++ b/test/api_integration/apis/kql_telemetry/kql_telemetry.ts @@ -49,8 +49,8 @@ export default function ({ getService }: FtrProviderContext) { q: 'type:kql-telemetry', }) .then((response) => { - const kqlTelemetryDoc = get(response, 'hits.hits[0]._source.kql-telemetry'); - expect(kqlTelemetryDoc.optInCount).to.be(1); + const optInCount = get(response, 'hits.hits[0]._source.kql-telemetry.optInCount'); + expect(optInCount).to.be(1); }); }); @@ -69,8 +69,8 @@ export default function ({ getService }: FtrProviderContext) { q: 'type:kql-telemetry', }) .then((response) => { - const kqlTelemetryDoc = get(response, 'hits.hits[0]._source.kql-telemetry'); - expect(kqlTelemetryDoc.optOutCount).to.be(1); + const optOutCount = get(response, 'hits.hits[0]._source.kql-telemetry.optOutCount'); + expect(optOutCount).to.be(1); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index 67b6784f8de54..dbd9b7b8b1e56 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -174,10 +174,10 @@ "@kbn/cloud-links-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_links/*"], "@kbn/cloud-plugin": ["x-pack/plugins/cloud"], "@kbn/cloud-plugin/*": ["x-pack/plugins/cloud/*"], - "@kbn/cloud-security-posture": ["x-pack/packages/kbn-cloud-security-posture"], - "@kbn/cloud-security-posture/*": ["x-pack/packages/kbn-cloud-security-posture/*"], - "@kbn/cloud-security-posture-common": ["x-pack/packages/kbn-cloud-security-posture-common"], - "@kbn/cloud-security-posture-common/*": ["x-pack/packages/kbn-cloud-security-posture-common/*"], + "@kbn/cloud-security-posture": ["x-pack/packages/kbn-cloud-security-posture/public"], + "@kbn/cloud-security-posture/*": ["x-pack/packages/kbn-cloud-security-posture/public/*"], + "@kbn/cloud-security-posture-common": ["x-pack/packages/kbn-cloud-security-posture/common"], + "@kbn/cloud-security-posture-common/*": ["x-pack/packages/kbn-cloud-security-posture/common/*"], "@kbn/cloud-security-posture-graph": ["x-pack/packages/kbn-cloud-security-posture/graph"], "@kbn/cloud-security-posture-graph/*": ["x-pack/packages/kbn-cloud-security-posture/graph/*"], "@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"], diff --git a/x-pack/examples/triggers_actions_ui_example/public/application.tsx b/x-pack/examples/triggers_actions_ui_example/public/application.tsx index 4a429fbfd58d7..b3c11beb5285c 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/application.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/application.tsx @@ -203,7 +203,6 @@ const TriggersActionsUiExampleApp = ({ ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry, actionTypeRegistry: triggersActionsUi.actionTypeRegistry, }} - returnUrl={application.getUrlForApp('triggersActionsUiExample')} /> )} @@ -229,7 +228,6 @@ const TriggersActionsUiExampleApp = ({ ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry, actionTypeRegistry: triggersActionsUi.actionTypeRegistry, }} - returnUrl={application.getUrlForApp('triggersActionsUiExample')} /> )} diff --git a/x-pack/packages/kbn-cloud-security-posture/README.md b/x-pack/packages/kbn-cloud-security-posture/README.md index 29a14fbeb825b..cf5450f059773 100644 --- a/x-pack/packages/kbn-cloud-security-posture/README.md +++ b/x-pack/packages/kbn-cloud-security-posture/README.md @@ -4,6 +4,20 @@ This package includes - Hooks that's used on Flyout component that's used in Alerts page on Security Solution Plugins as well as components on CSP plugin - Utilities and types thats used for the Hooks above as well as in CSP plugins +The code is under the `public` folder. + +# @kbn/cloud-security-posture-common + +Common types of `cloud-security-posture` plugin. + +The code is under the `common` folder. + +# @kbn/cloud-security-posture-graph + +Reusable graph component to present entities' relationships and exploration. + +The code is under the `graph` folder. + ## Storybook General look of the component can be checked visually running the following storybook: @@ -11,6 +25,6 @@ General look of the component can be checked visually running the following stor Note that all the interactions are mocked. -## Maintainers +# Maintainers Maintained by the Cloud Security Team \ No newline at end of file diff --git a/x-pack/packages/kbn-cloud-security-posture-common/README.md b/x-pack/packages/kbn-cloud-security-posture/common/README.md similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/README.md rename to x-pack/packages/kbn-cloud-security-posture/common/README.md diff --git a/x-pack/packages/kbn-cloud-security-posture-common/constants.ts b/x-pack/packages/kbn-cloud-security-posture/common/constants.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/constants.ts rename to x-pack/packages/kbn-cloud-security-posture/common/constants.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/index.ts b/x-pack/packages/kbn-cloud-security-posture/common/index.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/index.ts rename to x-pack/packages/kbn-cloud-security-posture/common/index.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/jest.config.js b/x-pack/packages/kbn-cloud-security-posture/common/jest.config.js similarity index 77% rename from x-pack/packages/kbn-cloud-security-posture-common/jest.config.js rename to x-pack/packages/kbn-cloud-security-posture/common/jest.config.js index d6f06d2bcc21c..62d5a239b9dc9 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/jest.config.js +++ b/x-pack/packages/kbn-cloud-security-posture/common/jest.config.js @@ -7,6 +7,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/packages/kbn-cloud-security-posture-common'], + rootDir: '../../../..', + roots: ['/x-pack/packages/kbn-cloud-security-posture/common'], }; diff --git a/x-pack/packages/kbn-cloud-security-posture-common/kibana.jsonc b/x-pack/packages/kbn-cloud-security-posture/common/kibana.jsonc similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/kibana.jsonc rename to x-pack/packages/kbn-cloud-security-posture/common/kibana.jsonc diff --git a/x-pack/packages/kbn-cloud-security-posture-common/package.json b/x-pack/packages/kbn-cloud-security-posture/common/package.json similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/package.json rename to x-pack/packages/kbn-cloud-security-posture/common/package.json diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/graph/index.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/index.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/graph/index.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/graph/index.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/graph/latest.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/latest.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/graph/latest.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/graph/latest.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/graph/v1.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/rules/index.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/rules/index.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/rules/index.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/rules/index.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/rules/latest.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/rules/latest.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/rules/latest.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/rules/latest.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v1.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v1.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v1.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v2.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v2.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v2.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v2.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v3.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v3.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v3.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v3.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v4.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v4.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v4.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v4.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v5.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v5.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/rules/v5.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/rules/v5.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/vulnerabilities/csp_vulnerability_finding.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/vulnerabilities/csp_vulnerability_finding.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/schema/vulnerabilities/latest.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/vulnerabilities/latest.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/schema/vulnerabilities/latest.ts rename to x-pack/packages/kbn-cloud-security-posture/common/schema/vulnerabilities/latest.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json similarity index 88% rename from x-pack/packages/kbn-cloud-security-posture-common/tsconfig.json rename to x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json index c6bdf82c1d223..c7cf1e9208bfc 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", "types": [ diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/benchmark.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/benchmark.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/types/benchmark.ts rename to x-pack/packages/kbn-cloud-security-posture/common/types/benchmark.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/findings.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/findings.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/types/findings.ts rename to x-pack/packages/kbn-cloud-security-posture/common/types/findings.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/graph/index.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/graph/index.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/types/graph/index.ts rename to x-pack/packages/kbn-cloud-security-posture/common/types/graph/index.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/graph/latest.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/graph/latest.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/types/graph/latest.ts rename to x-pack/packages/kbn-cloud-security-posture/common/types/graph/latest.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/types/graph/v1.ts rename to x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/status.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/status.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/types/status.ts rename to x-pack/packages/kbn-cloud-security-posture/common/types/status.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/vulnerabilities.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/vulnerabilities.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/types/vulnerabilities.ts rename to x-pack/packages/kbn-cloud-security-posture/common/types/vulnerabilities.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/get_abbreviated_number.test.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/get_abbreviated_number.test.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/utils/get_abbreviated_number.test.ts rename to x-pack/packages/kbn-cloud-security-posture/common/utils/get_abbreviated_number.test.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/get_abbreviated_number.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/get_abbreviated_number.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/utils/get_abbreviated_number.ts rename to x-pack/packages/kbn-cloud-security-posture/common/utils/get_abbreviated_number.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.test.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.test.ts rename to x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.ts rename to x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/ui_metrics.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture-common/utils/ui_metrics.ts rename to x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/README.md b/x-pack/packages/kbn-cloud-security-posture/graph/README.md index 2e495ce4e6be8..c67ca622fe414 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/README.md +++ b/x-pack/packages/kbn-cloud-security-posture/graph/README.md @@ -2,12 +2,12 @@ ## Motivation -The idea behind this package is to have a reusable graph component, embedding the features available to alerts flyout in +The idea behind this package is to have a reusable graph component, embedding the features available to the alert's flyout in security solution plugin. ## How to use this -Standalone examples will follow. In the meantime checkout storybook to view the graphs progress. +Standalone examples will follow. In the meantime check out storybook to view the graph's progress. ## The most important public api members diff --git a/x-pack/packages/kbn-cloud-security-posture/storybook/config/constants.ts b/x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/constants.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/storybook/config/constants.ts rename to x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/constants.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/storybook/config/index.ts b/x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/index.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/storybook/config/index.ts rename to x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/index.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/storybook/config/main.ts b/x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/main.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/storybook/config/main.ts rename to x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/main.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/storybook/config/manager.ts b/x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/manager.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/storybook/config/manager.ts rename to x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/manager.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/storybook/config/preview.ts b/x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/preview.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/storybook/config/preview.ts rename to x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/preview.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/storybook/config/styles.css b/x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/styles.css similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/storybook/config/styles.css rename to x-pack/packages/kbn-cloud-security-posture/graph/storybook/config/styles.css diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json index 55cafeb069d0f..d97809a59772d 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json @@ -12,6 +12,7 @@ ], "kbn_references": [ "@kbn/cloud-security-posture-common", - "@kbn/utility-types" + "@kbn/utility-types", + "@kbn/storybook" ] } diff --git a/x-pack/packages/kbn-cloud-security-posture/index.ts b/x-pack/packages/kbn-cloud-security-posture/public/index.ts similarity index 87% rename from x-pack/packages/kbn-cloud-security-posture/index.ts rename to x-pack/packages/kbn-cloud-security-posture/public/index.ts index b7e45a546f3d5..c39a86f5ec64b 100644 --- a/x-pack/packages/kbn-cloud-security-posture/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/index.ts @@ -5,9 +5,9 @@ * 2.0. */ -export * from './type'; -export * from './constants/component_constants'; -export * from './constants/navigation'; +export * from './src/types'; +export * from './src/constants/component_constants'; +export * from './src/constants/navigation'; export type { NavFilter } from './src/hooks/use_navigate_findings'; export { showErrorToast } from './src/utils/show_error_toast'; export { encodeQuery, decodeQuery } from './src/utils/query_utils'; diff --git a/x-pack/packages/kbn-cloud-security-posture/jest.config.js b/x-pack/packages/kbn-cloud-security-posture/public/jest.config.js similarity index 74% rename from x-pack/packages/kbn-cloud-security-posture/jest.config.js rename to x-pack/packages/kbn-cloud-security-posture/public/jest.config.js index 7273e0452cf60..1c1a8c84561c1 100644 --- a/x-pack/packages/kbn-cloud-security-posture/jest.config.js +++ b/x-pack/packages/kbn-cloud-security-posture/public/jest.config.js @@ -7,6 +7,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/packages/kbn-cloud-security-posture'], + rootDir: '../../../..', + roots: ['/x-pack/packages/kbn-cloud-security-posture/public'], }; diff --git a/x-pack/packages/kbn-cloud-security-posture/kibana.jsonc b/x-pack/packages/kbn-cloud-security-posture/public/kibana.jsonc similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/kibana.jsonc rename to x-pack/packages/kbn-cloud-security-posture/public/kibana.jsonc diff --git a/x-pack/packages/kbn-cloud-security-posture/package.json b/x-pack/packages/kbn-cloud-security-posture/public/package.json similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/package.json rename to x-pack/packages/kbn-cloud-security-posture/public/package.json diff --git a/x-pack/packages/kbn-cloud-security-posture/src/components/csp_evaluation_badge.tsx b/x-pack/packages/kbn-cloud-security-posture/public/src/components/csp_evaluation_badge.tsx similarity index 95% rename from x-pack/packages/kbn-cloud-security-posture/src/components/csp_evaluation_badge.tsx rename to x-pack/packages/kbn-cloud-security-posture/public/src/components/csp_evaluation_badge.tsx index 8048414c8f8a5..f84ac26d68767 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/components/csp_evaluation_badge.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/components/csp_evaluation_badge.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiBadge, type EuiBadgeProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; -import { statusColors } from '../../constants/component_constants'; +import { statusColors } from '../constants/component_constants'; interface Props { type?: 'passed' | 'failed'; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/components/vulnerability_badges.tsx b/x-pack/packages/kbn-cloud-security-posture/public/src/components/vulnerability_badges.tsx similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/components/vulnerability_badges.tsx rename to x-pack/packages/kbn-cloud-security-posture/public/src/components/vulnerability_badges.tsx diff --git a/x-pack/packages/kbn-cloud-security-posture/constants/component_constants.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/constants/component_constants.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/constants/component_constants.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/constants/component_constants.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/constants/navigation.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/constants/navigation.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/constants/navigation.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/constants/navigation.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_csp_setup_status_api.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_csp_setup_status_api.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_csp_setup_status_api.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_csp_setup_status_api.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_data_view.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_data_view.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_data_view.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_data_view.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_get_benchmark_rules_state_api.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_get_benchmark_rules_state_api.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_findings.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts similarity index 99% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_findings.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts index fe25224cf417a..40880b132537d 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_findings.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts @@ -15,7 +15,7 @@ import type { LatestFindingsRequest, LatestFindingsResponse, UseCspOptions, -} from '../../type'; +} from '../types'; import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api'; import { diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_preview.ts similarity index 98% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_preview.ts index 75bd0d3952bd7..067cd22a9e1a9 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_preview.ts @@ -14,7 +14,7 @@ import type { LatestFindingsRequest, LatestFindingsResponse, UseCspOptions, -} from '../../type'; +} from '../types'; import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api'; import { buildMisconfigurationsFindingsQuery, diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_navigate_findings.test.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.test.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_navigate_findings.test.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.test.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_navigate_findings.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.ts similarity index 97% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_navigate_findings.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.ts index a8420b17dd4f9..454c9a0056a58 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_navigate_findings.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_navigate_findings.ts @@ -14,7 +14,7 @@ import { } from '@kbn/cloud-security-posture-common'; import type { CoreStart } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { findingsNavigation } from '../../constants/navigation'; +import { findingsNavigation } from '../constants/navigation'; import { useDataView } from './use_data_view'; import { CspClientPluginStartDeps } from '../..'; import { encodeQuery } from '../utils/query_utils'; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_findings.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_findings.ts similarity index 99% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_findings.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_findings.ts index ba13ec983893f..062f5c7740c73 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_findings.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_findings.ts @@ -16,7 +16,7 @@ import { } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; import type { CoreStart } from '@kbn/core/public'; -import type { CspClientPluginStartDeps, UseCspOptions } from '../../type'; +import type { CspClientPluginStartDeps, UseCspOptions } from '../types'; import { showErrorToast } from '../..'; import { getVulnerabilitiesAggregationCount, getVulnerabilitiesQuery } from '../utils/hooks_utils'; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_preview.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_preview.ts similarity index 99% rename from x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_preview.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_preview.ts index 82b3f41d26819..ac34636720143 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_vulnerabilities_preview.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_vulnerabilities_preview.ts @@ -16,7 +16,7 @@ import { } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; import type { CoreStart } from '@kbn/core/public'; -import type { CspClientPluginStartDeps, UseCspOptions } from '../../type'; +import type { CspClientPluginStartDeps, UseCspOptions } from '../types'; import { showErrorToast } from '../..'; import { getVulnerabilitiesAggregationCount, getVulnerabilitiesQuery } from '../utils/hooks_utils'; diff --git a/x-pack/packages/kbn-cloud-security-posture/type.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/types.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/type.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/types.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerabilitiy_colors.test.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerabilitiy_colors.test.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerabilitiy_colors.test.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerabilitiy_colors.test.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_colors.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerability_colors.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_colors.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerability_colors.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_text.test.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerability_text.test.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_text.test.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerability_text.test.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_text.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerability_text.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/get_vulnerability_text.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/get_vulnerability_text.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.test.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/hooks_utils.test.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.test.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/hooks_utils.test.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/hooks_utils.ts similarity index 99% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/hooks_utils.ts index d06f4efbde026..a621e1d01add8 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/hooks_utils.ts @@ -13,7 +13,7 @@ import { } from '@kbn/cloud-security-posture-common'; import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import { buildMutedRulesFilter } from '@kbn/cloud-security-posture-common'; -import type { UseCspOptions } from '../../type'; +import type { UseCspOptions } from '../types'; const MISCONFIGURATIONS_SOURCE_FIELDS = ['result.*', 'rule.*', 'resource.*']; interface AggregationBucket { diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/query_utils.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/query_utils.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/query_utils.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/query_utils.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/show_error_toast.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/show_error_toast.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/show_error_toast.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/show_error_toast.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.test.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/vulnerability_helpers.test.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.test.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/vulnerability_helpers.test.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/utils/vulnerability_helpers.ts similarity index 100% rename from x-pack/packages/kbn-cloud-security-posture/src/utils/vulnerability_helpers.ts rename to x-pack/packages/kbn-cloud-security-posture/public/src/utils/vulnerability_helpers.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/public/tsconfig.json similarity index 92% rename from x-pack/packages/kbn-cloud-security-posture/tsconfig.json rename to x-pack/packages/kbn-cloud-security-posture/public/tsconfig.json index 38799e07182d9..e7f69a99c5199 100644 --- a/x-pack/packages/kbn-cloud-security-posture/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/public/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", }, @@ -35,6 +35,5 @@ "@kbn/ui-theme", "@kbn/i18n-react", "@kbn/rison", - "@kbn/storybook", ] } diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts new file mode 100644 index 0000000000000..8e0fc6ad09479 --- /dev/null +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { joinByKey } from './join_by_key'; + +describe('joinByKey', () => { + it('joins by a string key', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + avg: 10, + }, + { + serviceName: 'opbeans-node', + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + }, + { + serviceName: 'opbeans-java', + p95: 18, + }, + ], + 'serviceName' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + serviceName: 'opbeans-node', + avg: 10, + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + p95: 18, + }, + ]); + }); + + it('joins by a record key', () => { + const joined = joinByKey( + [ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + }, + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + p95: 18, + }, + ], + 'key' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + p95: 18, + }, + ]); + }); + + it('joins by multiple keys', () => { + const data = [ + { + serviceName: 'opbeans-node', + environment: 'production', + type: 'service', + }, + { + serviceName: 'opbeans-node', + environment: 'stage', + type: 'service', + }, + { + serviceName: 'opbeans-node', + hostName: 'host-1', + }, + { + containerId: 'containerId', + }, + ]; + + const alerts = [ + { + serviceName: 'opbeans-node', + environment: 'production', + type: 'service', + alertCount: 10, + }, + { + containerId: 'containerId', + alertCount: 1, + }, + { + hostName: 'host-1', + environment: 'production', + alertCount: 5, + }, + ]; + + const joined = joinByKey( + [...data, ...alerts], + ['serviceName', 'environment', 'hostName', 'containerId'] + ); + + expect(joined.length).toBe(5); + + expect(joined).toEqual([ + { environment: 'stage', serviceName: 'opbeans-node', type: 'service' }, + { hostName: 'host-1', serviceName: 'opbeans-node' }, + { alertCount: 10, environment: 'production', serviceName: 'opbeans-node', type: 'service' }, + { alertCount: 1, containerId: 'containerId' }, + { alertCount: 5, environment: 'production', hostName: 'host-1' }, + ]); + }); + + it('uses the custom merge fn to replace items', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-java', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['b'], + }, + { + serviceName: 'opbeans-node', + values: ['c'], + }, + ], + 'serviceName', + (a, b) => ({ + ...a, + ...b, + values: a.values.concat(b.values), + }) + ); + + expect(joined.find((item) => item.serviceName === 'opbeans-node')?.values).toEqual([ + 'a', + 'b', + 'c', + ]); + }); + + it('deeply merges objects', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + properties: { + foo: '', + }, + }, + { + serviceName: 'opbeans-node', + properties: { + bar: '', + }, + }, + ], + 'serviceName' + ); + + expect(joined[0]).toEqual({ + serviceName: 'opbeans-node', + properties: { + foo: '', + bar: '', + }, + }); + }); +}); diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.ts new file mode 100644 index 0000000000000..54e8ecdaf409b --- /dev/null +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UnionToIntersection, ValuesType } from 'utility-types'; +import { merge, castArray } from 'lodash'; +import stableStringify from 'json-stable-stringify'; + +export type JoinedReturnType< + T extends Record, + U extends UnionToIntersection +> = Array< + Partial & { + [k in keyof T]: T[k]; + } +>; + +type ArrayOrSingle = T | T[]; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends ArrayOrSingle +>(items: T[], key: V): JoinedReturnType; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends ArrayOrSingle, + W extends JoinedReturnType, + X extends (a: T, b: T) => ValuesType +>(items: T[], key: V, mergeFn: X): W; + +export function joinByKey( + items: Array>, + key: string | string[], + mergeFn: Function = (a: Record, b: Record) => merge({}, a, b) +) { + const keys = castArray(key); + // Create a map to quickly query the key of group. + const map = new Map(); + items.forEach((current) => { + // The key of the map is a stable JSON string of the values from given keys. + // We need stable JSON string to support plain object values. + const stableKey = stableStringify(keys.map((k) => current[k])); + + if (map.has(stableKey)) { + const item = map.get(stableKey); + // delete and set the key to put it last + map.delete(stableKey); + map.set(stableKey, mergeFn(item, current)); + } else { + map.set(stableKey, { ...current }); + } + }); + return [...map.values()]; +} diff --git a/x-pack/packages/observability/observability_utils/object/flatten_object.test.ts b/x-pack/packages/observability/observability_utils/object/flatten_object.test.ts index deb7ed998c478..13a8174f4f1cf 100644 --- a/x-pack/packages/observability/observability_utils/object/flatten_object.test.ts +++ b/x-pack/packages/observability/observability_utils/object/flatten_object.test.ts @@ -21,6 +21,18 @@ describe('flattenObject', () => { }); }); + it('flattens arrays', () => { + expect( + flattenObject({ + child: { + id: [1, 2], + }, + }) + ).toEqual({ + 'child.id': [1, 2], + }); + }); + it('does not flatten arrays', () => { expect( flattenObject({ diff --git a/x-pack/packages/observability/observability_utils/object/unflatten_object.test.ts b/x-pack/packages/observability/observability_utils/object/unflatten_object.test.ts new file mode 100644 index 0000000000000..22cee17bb1a64 --- /dev/null +++ b/x-pack/packages/observability/observability_utils/object/unflatten_object.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { unflattenObject } from './unflatten_object'; + +describe('unflattenObject', () => { + it('unflattens deeply nested objects', () => { + expect(unflattenObject({ 'first.second.third': 'third' })).toEqual({ + first: { + second: { + third: 'third', + }, + }, + }); + }); + + it('does not unflatten arrays', () => { + expect( + unflattenObject({ + simpleArray: ['0', '1', '2'], + complexArray: [{ one: 'one', two: 'two', three: 'three' }], + 'nested.array': [0, 1, 2], + 'complex.nested': [{ one: 'one', two: 'two', 'first.second': 'foo', 'first.third': 'bar' }], + }) + ).toEqual({ + simpleArray: ['0', '1', '2'], + complexArray: [{ one: 'one', two: 'two', three: 'three' }], + nested: { + array: [0, 1, 2], + }, + complex: { + nested: [{ one: 'one', two: 'two', first: { second: 'foo', third: 'bar' } }], + }, + }); + }); +}); diff --git a/x-pack/packages/observability/observability_utils/object/unflatten_object.ts b/x-pack/packages/observability/observability_utils/object/unflatten_object.ts new file mode 100644 index 0000000000000..142ea2eea6461 --- /dev/null +++ b/x-pack/packages/observability/observability_utils/object/unflatten_object.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { set } from '@kbn/safer-lodash-set'; + +export function unflattenObject(source: Record, target: Record = {}) { + // eslint-disable-next-line guard-for-in + for (const key in source) { + const val = source[key as keyof typeof source]; + + if (Array.isArray(val)) { + const unflattenedArray = val.map((item) => { + if (item && typeof item === 'object' && !Array.isArray(item)) { + return unflattenObject(item); + } + return item; + }); + set(target, key, unflattenedArray); + } else { + set(target, key, val); + } + } + return target; +} diff --git a/x-pack/packages/observability/observability_utils/tsconfig.json b/x-pack/packages/observability/observability_utils/tsconfig.json index 2ed47d10cfad9..b3f1a4a21c4e7 100644 --- a/x-pack/packages/observability/observability_utils/tsconfig.json +++ b/x-pack/packages/observability/observability_utils/tsconfig.json @@ -21,5 +21,6 @@ "@kbn/es-types", "@kbn/apm-utils", "@kbn/es-query", + "@kbn/safer-lodash-set", ] } diff --git a/x-pack/packages/security/role_management_model/src/kibana_privilege.ts b/x-pack/packages/security/role_management_model/src/kibana_privilege.ts index f38d60417e72b..69441cebe0c78 100644 --- a/x-pack/packages/security/role_management_model/src/kibana_privilege.ts +++ b/x-pack/packages/security/role_management_model/src/kibana_privilege.ts @@ -10,7 +10,7 @@ import _ from 'lodash'; export class KibanaPrivilege { constructor(public readonly id: string, public readonly actions: string[] = []) {} - public get name() { + public get name(): string { return _.upperFirst(this.id); } diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/sanitize_bulk_response.ts b/x-pack/plugins/alerting/server/alerts_client/lib/sanitize_bulk_response.ts index 2b6d9f6e3c2c3..0c18500c3bd5f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/sanitize_bulk_response.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/sanitize_bulk_response.ts @@ -24,10 +24,10 @@ export const sanitizeBulkErrorResponse = ( (responseToUse.items ?? []).forEach( (item: Partial>) => { for (const [_, responseItem] of Object.entries(item)) { - const reason: string = get(responseItem, 'error.reason'); + const reason = get(responseItem, 'error.reason'); const redactIndex = reason ? reason.indexOf(`Preview of field's value:`) : -1; if (redactIndex > 1) { - set(responseItem, 'error.reason', reason.substring(0, redactIndex - 1)); + set(responseItem, 'error.reason', reason!.substring(0, redactIndex - 1)); } } } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx index 41fdc5709882c..e8710bcbd1daa 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx @@ -52,7 +52,7 @@ export class ExtendedTemplate extends PureComponent { // TODO: this should be in a helper, it's the same code from container_style getArgValue = (name: string, alt: string) => { - return get(this.props.argValue, `chain.0.arguments.${name}.0`, alt); + return get(this.props.argValue, `chain.0.arguments.${name}.0`, alt) as string | undefined; }; // TODO: this should be in a helper, it's the same code from container_style diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx index 2e7007e8d2551..8884533fb7ce3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx @@ -46,9 +46,9 @@ const PERCENT_DECIMALS_FIELD = 'percentDecimals'; export const ExtendedTemplate: FunctionComponent = ({ onValueChange, argValue }) => { const showLabels = getFieldValue(argValue, SHOW_FIELD); - const showValues = getFieldValue(argValue, VALUES_FIELD); - const valueFormat = getFieldValue(argValue, VALUES_FORMAT_FIELD); - const percentDecimals = getFieldValue(argValue, PERCENT_DECIMALS_FIELD); + const showValues = getFieldValue(argValue, VALUES_FIELD) as boolean; + const valueFormat = getFieldValue(argValue, VALUES_FORMAT_FIELD) as string; + const percentDecimals = getFieldValue(argValue, PERCENT_DECIMALS_FIELD) as string; const positions: EuiSelectOption[] = [ { text: strings.getPositionDefaultLabel(), value: 'default' }, @@ -110,7 +110,7 @@ export const ExtendedTemplate: FunctionComponent = ({ onValueChange, argV diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/simple_template.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/simple_template.tsx index 08e27fbcd0988..eb98e0a3e0a2c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/simple_template.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/simple_template.tsx @@ -39,7 +39,7 @@ export const SimpleTemplate: FunctionComponent = ({ onValueChange, argVal [argValue, onValueChange, showValuePath] ); - const showLabels = getFieldValue(argValue, SHOW_FIELD, false); + const showLabels = getFieldValue(argValue, SHOW_FIELD, false) as boolean; return ( diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/utils.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/utils.ts index 5aeffe9c6961f..974e4ef20a299 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/utils.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/utils.ts @@ -18,7 +18,7 @@ export const getFieldValue = ( defaultValue?: unknown ) => { if (!ast) { - return null; + return undefined; } return get(ast, getFieldPath(field), defaultValue); diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx index 2ffea6c87ea1b..67322064fcc2f 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx @@ -45,7 +45,7 @@ export const ExtendedTemplate: FunctionComponent = (props) => { } = props; const chain = get(argValue, 'chain.0', {}); const chainArgs = get(chain, 'arguments', {}); - const selectedSeries = get(chainArgs, 'label.0', ''); + const selectedSeries = get(chainArgs, 'label.0', '') as string; let name = ''; if (typeInstance) { @@ -101,7 +101,7 @@ export const ExtendedTemplate: FunctionComponent = (props) => { handleChange('lines', ev)} @@ -113,7 +113,7 @@ export const ExtendedTemplate: FunctionComponent = (props) => { handleChange('bars', ev)} @@ -125,7 +125,7 @@ export const ExtendedTemplate: FunctionComponent = (props) => { handleChange('points', ev)} diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/index.ts b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/index.ts index 140919ed29c04..d0ac9f184aeaa 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/index.ts +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/index.ts @@ -33,13 +33,13 @@ const formatLabel = (label: string, props: Props) => { const EnhancedExtendedTemplate = compose( lifecycle({ componentWillMount() { - const label = get(this.props.argValue, 'chain.0.arguments.label.0', ''); + const label = get(this.props.argValue, 'chain.0.arguments.label.0', '') as string; if (label) { this.props.setLabel(formatLabel(label, this.props)); } }, componentDidUpdate(prevProps) { - const newLabel = get(this.props.argValue, 'chain.0.arguments.label.0', ''); + const newLabel = get(this.props.argValue, 'chain.0.arguments.label.0', '') as string; if (newLabel && prevProps.label !== formatLabel(newLabel, this.props)) { this.props.setLabel(formatLabel(newLabel, this.props)); } diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx index d54e6fdb60040..7344a3f686e93 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx @@ -44,7 +44,7 @@ export const SimpleTemplate: FunctionComponent = (props) => { const { name } = typeInstance; const chain = get(argValue, 'chain.0', {}); const chainArgs = get(chain, 'arguments', {}); - const color: string = get(chainArgs, 'color.0', ''); + const color: string = get(chainArgs, 'color.0', '') as string; const handleChange: (key: T, val: string) => void = (argName, val) => { const fn = val === '' ? del : set; diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index a37953657e157..97049f8af57c8 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -27,14 +27,14 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = // remove all allFilters that belong to a group return allFilters.filter((filter: string) => { const ast = fromExpression(filter); - const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []) as string[]; return expGroups.length === 0; }); } return allFilters.filter((filter: string) => { const ast = fromExpression(filter); - const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []) as string[]; return expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup)); }); } diff --git a/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts b/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts index 2d58e926d28c8..ddbbb62bd1872 100644 --- a/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts +++ b/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts @@ -38,7 +38,7 @@ export const getFlotAxisConfig = ( const config: Config = { show: true }; - const axisType = get(columns, `${axis}.type`); + const axisType = get(columns, [axis, `type`]); if (isAxisConfig(argValue)) { const { position, min, max, tickSize } = argValue; diff --git a/x-pack/plugins/canvas/public/lib/filter.ts b/x-pack/plugins/canvas/public/lib/filter.ts index 767cf53e16f6d..fade544b2bc80 100644 --- a/x-pack/plugins/canvas/public/lib/filter.ts +++ b/x-pack/plugins/canvas/public/lib/filter.ts @@ -65,7 +65,7 @@ const excludeFiltersByGroups = (filters: Ast[], filterExprAst: AstFunction) => { const groupsToExclude = filterExprAst.arguments.group ?? []; const removeUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; return filters.filter((filter) => { - const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + const groups: string[] = (get(filter, 'chain[0].arguments.filterGroup', []) as string[]).filter( (group: string) => group !== '' ); const noNeedToExcludeByGroup = !( @@ -89,7 +89,7 @@ const includeFiltersByGroups = ( const groupsToInclude = filterExprAst.arguments.group ?? []; const includeOnlyUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; return filters.filter((filter) => { - const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + const groups: string[] = (get(filter, 'chain[0].arguments.filterGroup', []) as string[]).filter( (group: string) => group !== '' ); const needToIncludeByGroup = diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts index 9444b7b4b1922..be2a7c75aad46 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts @@ -10,7 +10,7 @@ import { KSPM_POLICY_TEMPLATE, CLOUD_SECURITY_POSTURE_BASE_PATH, } from '@kbn/cloud-security-posture-common'; -import { NAV_ITEMS_NAMES } from '@kbn/cloud-security-posture/constants/navigation'; +import { NAV_ITEMS_NAMES } from '@kbn/cloud-security-posture/src/constants/navigation'; import { CNVM_POLICY_TEMPLATE } from '../../../common/constants'; import type { CspBenchmarksPage, CspPage, CspPageNavigationItem } from './types'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx index 01309ce334d3c..8c75496e04c7d 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx @@ -17,6 +17,7 @@ import { METRIC_TYPE } from '@kbn/analytics'; import { useHistory } from 'react-router-dom'; import useSessionStorage from 'react-use/lib/useSessionStorage'; import { useQueryClient } from '@tanstack/react-query'; +import { i18n as kbnI18n } from '@kbn/i18n'; import { useFetchDetectionRulesAlertsStatus } from '../common/api/use_fetch_detection_rules_alerts_status'; import { useFetchDetectionRulesByTags } from '../common/api/use_fetch_detection_rules_by_tags'; import { RuleResponse } from '../common/types'; @@ -67,15 +68,30 @@ export const DetectionRuleCounter = ({ tags, createRuleFn }: DetectionRuleCounte }, [history]); const createDetectionRuleOnClick = useCallback(async () => { - uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, CREATE_DETECTION_RULE_FROM_FLYOUT); const startServices = { analytics, notifications, i18n, theme }; - setIsCreateRuleLoading(true); - const ruleResponse = await createRuleFn(http); - setIsCreateRuleLoading(false); - showCreateDetectionRuleSuccessToast(startServices, http, ruleResponse); - // Triggering a refetch of rules and alerts to update the UI - queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]); - queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]); + + try { + setIsCreateRuleLoading(true); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, CREATE_DETECTION_RULE_FROM_FLYOUT); + + const ruleResponse = await createRuleFn(http); + + setIsCreateRuleLoading(false); + showCreateDetectionRuleSuccessToast(startServices, http, ruleResponse); + + // Triggering a refetch of rules and alerts to update the UI + queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]); + queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]); + } catch (e) { + setIsCreateRuleLoading(false); + + notifications.toasts.addWarning({ + title: kbnI18n.translate('xpack.csp.detectionRuleCounter.alerts.createRuleErrorTitle', { + defaultMessage: 'Coming Soon', + }), + text: e.message, + }); + } }, [createRuleFn, http, analytics, notifications, i18n, theme, queryClient]); if (alertsIsError) return <>{'-'}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_benchmark.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_benchmark.ts index 0ce1b7d09e897..cd09f275aaf22 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_benchmark.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/create_detection_rule_from_benchmark.ts @@ -8,8 +8,8 @@ import { HttpSetup } from '@kbn/core/public'; import { LATEST_FINDINGS_RETENTION_POLICY } from '@kbn/cloud-security-posture-common'; import type { CspBenchmarkRule } from '@kbn/cloud-security-posture-common/schema/rules/latest'; +import { i18n } from '@kbn/i18n'; import { FINDINGS_INDEX_PATTERN } from '../../../../common/constants'; - import { createDetectionRule } from '../../../common/api/create_detection_rule'; import { generateBenchmarkRuleTags } from '../../../../common/utils/detection_rules'; @@ -63,6 +63,14 @@ export const createDetectionRuleFromBenchmarkRule = async ( http: HttpSetup, benchmarkRule: CspBenchmarkRule['metadata'] ) => { + if (!benchmarkRule.benchmark?.posture_type) { + throw new Error( + i18n.translate('xpack.csp.createDetectionRuleFromBenchmarkRule.createRuleErrorMessage', { + defaultMessage: 'Rule creation is currently only available for Elastic findings', + }) + ); + } + return await createDetectionRule({ http, rule: { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts index 7dd0982cc58b5..4558d78fb8cf9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts @@ -18,7 +18,7 @@ jest.mock('../../../common/utils/is_native_csp_finding', () => ({ isNativeCspFinding: jest.fn(), })); -describe('CreateDetectionRuleFromVulnerability', () => { +describe.skip('CreateDetectionRuleFromVulnerability', () => { describe('getVulnerabilityTags', () => { it('should return tags with CSP_RULE_TAG and vulnerability id', () => { const mockVulnerability = { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts index 804e89fad61d8..bf01180c38789 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts @@ -13,6 +13,7 @@ import { VULNERABILITIES_SEVERITY, } from '@kbn/cloud-security-posture-common'; import type { Vulnerability } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; +import { CSP_VULN_DATASET } from '../../../common/utils/get_vendor_name'; import { isNativeCspFinding } from '../../../common/utils/is_native_csp_finding'; import { VULNERABILITIES_INDEX_PATTERN } from '../../../../common/constants'; import { createDetectionRule } from '../../../common/api/create_detection_rule'; @@ -87,6 +88,15 @@ export const createDetectionRuleFromVulnerabilityFinding = async ( http: HttpSetup, vulnerabilityFinding: CspVulnerabilityFinding ) => { + if (vulnerabilityFinding.data_stream?.dataset !== CSP_VULN_DATASET) { + throw new Error( + i18n.translate( + 'xpack.csp.createDetectionRuleFromVulnerabilityFinding.createRuleErrorMessage', + { defaultMessage: 'Rule creation is currently only available for Elastic findings' } + ) + ); + } + const tags = getVulnerabilityTags(vulnerabilityFinding); const vulnerability = vulnerabilityFinding.vulnerability; diff --git a/x-pack/plugins/data_quality/common/index.ts b/x-pack/plugins/data_quality/common/index.ts index a1869cd9ac356..f6de79310eff5 100644 --- a/x-pack/plugins/data_quality/common/index.ts +++ b/x-pack/plugins/data_quality/common/index.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; export const PLUGIN_ID = 'data_quality'; +export const PLUGIN_FEATURE_ID = 'dataQuality'; export const PLUGIN_NAME = i18n.translate('xpack.dataQuality.name', { defaultMessage: 'Data Set Quality', }); diff --git a/x-pack/plugins/data_quality/public/plugin.ts b/x-pack/plugins/data_quality/public/plugin.ts index 025268848a9a8..27639f896ab60 100644 --- a/x-pack/plugins/data_quality/public/plugin.ts +++ b/x-pack/plugins/data_quality/public/plugin.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { Capabilities, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public'; import { MANAGEMENT_APP_LOCATOR } from '@kbn/deeplinks-management/constants'; import { ManagementAppLocatorParams } from '@kbn/management-plugin/common/locator'; +import { Subject } from 'rxjs'; import { DataQualityPluginSetup, DataQualityPluginStart, @@ -30,6 +31,8 @@ export class DataQualityPlugin AppPluginStartDependencies > { + private capabilities$ = new Subject(); + public setup( core: CoreSetup, plugins: AppPluginSetupDependencies @@ -37,51 +40,56 @@ export class DataQualityPlugin const { management, share } = plugins; const useHash = core.uiSettings.get('state:storeInSessionStorage'); - management.sections.section.data.registerApp({ - id: PLUGIN_ID, - title: PLUGIN_NAME, - order: 2, - keywords: [ - 'data', - 'quality', - 'data quality', - 'datasets', - 'datasets quality', - 'data set quality', - ], - async mount(params: ManagementAppMountParams) { - const [{ renderApp }, [coreStart, pluginsStartDeps, pluginStart]] = await Promise.all([ - import('./application'), - core.getStartServices(), - ]); + this.capabilities$.subscribe((capabilities) => { + if (!capabilities.dataQuality.show) return; - return renderApp(coreStart, pluginsStartDeps, pluginStart, params); - }, - hideFromSidebar: false, - }); + management.sections.section.data.registerApp({ + id: PLUGIN_ID, + title: PLUGIN_NAME, + order: 2, + keywords: [ + 'data', + 'quality', + 'data quality', + 'datasets', + 'datasets quality', + 'data set quality', + ], + async mount(params: ManagementAppMountParams) { + const [{ renderApp }, [coreStart, pluginsStartDeps, pluginStart]] = await Promise.all([ + import('./application'), + core.getStartServices(), + ]); - const managementLocator = - share.url.locators.get(MANAGEMENT_APP_LOCATOR); + return renderApp(coreStart, pluginsStartDeps, pluginStart, params); + }, + hideFromSidebar: false, + }); - if (managementLocator) { - share.url.locators.create( - new DatasetQualityLocatorDefinition({ - useHash, - managementLocator, - }) - ); - share.url.locators.create( - new DatasetQualityDetailsLocatorDefinition({ - useHash, - managementLocator, - }) - ); - } + const managementLocator = + share.url.locators.get(MANAGEMENT_APP_LOCATOR); + + if (managementLocator) { + share.url.locators.create( + new DatasetQualityLocatorDefinition({ + useHash, + managementLocator, + }) + ); + share.url.locators.create( + new DatasetQualityDetailsLocatorDefinition({ + useHash, + managementLocator, + }) + ); + } + }); return {}; } - public start(_core: CoreStart): DataQualityPluginStart { + public start(core: CoreStart): DataQualityPluginStart { + this.capabilities$.next(core.application.capabilities); return {}; } diff --git a/x-pack/plugins/data_quality/server/features.ts b/x-pack/plugins/data_quality/server/features.ts new file mode 100644 index 0000000000000..a570c78e6edbe --- /dev/null +++ b/x-pack/plugins/data_quality/server/features.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { + KibanaFeatureConfig, + KibanaFeatureScope, + ElasticsearchFeatureConfig, +} from '@kbn/features-plugin/common'; +import { PLUGIN_FEATURE_ID, PLUGIN_ID, PLUGIN_NAME } from '../common'; + +export const KIBANA_FEATURE: KibanaFeatureConfig = { + id: PLUGIN_FEATURE_ID, + name: PLUGIN_NAME, + category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [PLUGIN_ID], + privileges: { + all: { + app: [PLUGIN_ID], + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + read: { + disabled: true, + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + }, +}; + +export const ELASTICSEARCH_FEATURE: ElasticsearchFeatureConfig = { + id: PLUGIN_ID, + management: { + data: [PLUGIN_ID], + }, + privileges: [ + { + ui: [], + requiredClusterPrivileges: [], + requiredIndexPrivileges: { + ['logs-*-*']: ['read'], + }, + }, + { + ui: [], + requiredClusterPrivileges: [], + requiredIndexPrivileges: { + ['traces-*-*']: ['read'], + }, + }, + { + ui: [], + requiredClusterPrivileges: [], + requiredIndexPrivileges: { + ['metrics-*-*']: ['read'], + }, + }, + { + ui: [], + requiredClusterPrivileges: [], + requiredIndexPrivileges: { + ['synthetics-*-*']: ['read'], + }, + }, + ], +}; diff --git a/x-pack/plugins/data_quality/server/plugin.ts b/x-pack/plugins/data_quality/server/plugin.ts index 1b7e9cface597..93ed93917fa7a 100644 --- a/x-pack/plugins/data_quality/server/plugin.ts +++ b/x-pack/plugins/data_quality/server/plugin.ts @@ -6,48 +6,14 @@ */ import { CoreSetup, Plugin } from '@kbn/core/server'; -import { PLUGIN_ID } from '../common'; import { Dependencies } from './types'; +import { ELASTICSEARCH_FEATURE, KIBANA_FEATURE } from './features'; export class DataQualityPlugin implements Plugin { - public setup(coreSetup: CoreSetup, { features }: Dependencies) { - features.registerElasticsearchFeature({ - id: PLUGIN_ID, - management: { - data: [PLUGIN_ID], - }, - privileges: [ - { - ui: [], - requiredClusterPrivileges: [], - requiredIndexPrivileges: { - ['logs-*-*']: ['read'], - }, - }, - { - ui: [], - requiredClusterPrivileges: [], - requiredIndexPrivileges: { - ['traces-*-*']: ['read'], - }, - }, - { - ui: [], - requiredClusterPrivileges: [], - requiredIndexPrivileges: { - ['metrics-*-*']: ['read'], - }, - }, - { - ui: [], - requiredClusterPrivileges: [], - requiredIndexPrivileges: { - ['synthetics-*-*']: ['read'], - }, - }, - ], - }); + public setup(_coreSetup: CoreSetup, { features }: Dependencies) { + features.registerKibanaFeature(KIBANA_FEATURE); + features.registerElasticsearchFeature(ELASTICSEARCH_FEATURE); } public start() {} diff --git a/x-pack/plugins/data_quality/tsconfig.json b/x-pack/plugins/data_quality/tsconfig.json index 911c4fbfff557..a3f04f88ec7ff 100644 --- a/x-pack/plugins/data_quality/tsconfig.json +++ b/x-pack/plugins/data_quality/tsconfig.json @@ -28,6 +28,7 @@ "@kbn/deeplinks-management", "@kbn/deeplinks-observability", "@kbn/ebt-tools", + "@kbn/core-application-common", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/data_visualizer/common/constants.ts b/x-pack/plugins/data_visualizer/common/constants.ts index ff277b9bb4785..4f552f45f61a4 100644 --- a/x-pack/plugins/data_visualizer/common/constants.ts +++ b/x-pack/plugins/data_visualizer/common/constants.ts @@ -47,6 +47,9 @@ export const SUPPORTED_FIELD_TYPES = { NESTED: 'nested', STRING: 'string', TEXT: 'text', + SEMANTIC_TEXT: 'semantic_text', + DENSE_VECTOR: 'dense_vector', + SPARSE_VECTOR: 'sparse_vector', VERSION: 'version', UNKNOWN: 'unknown', } as const; @@ -73,3 +76,4 @@ export const featureTitle = i18n.translate('xpack.dataVisualizer.title', { defaultMessage: 'Upload a file', }); export const featureId = `file_data_visualizer`; +export const SUPPORTED_FIELD_TYPES_LIST: string[] = Object.values(SUPPORTED_FIELD_TYPES); diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts index 0b79cd4079f76..ad9aaf6413813 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts @@ -8,7 +8,7 @@ import type { DataViewField } from '@kbn/data-views-plugin/public'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { getFieldType } from '@kbn/field-utils/src/utils/get_field_type'; -import { SUPPORTED_FIELD_TYPES } from '../../../../common/constants'; +import { SUPPORTED_FIELD_TYPES, SUPPORTED_FIELD_TYPES_LIST } from '../../../../common/constants'; // convert kibana types to ML Job types // this is needed because kibana types only have string and not text and keyword. @@ -26,6 +26,15 @@ export function kbnTypeToSupportedType(field: DataViewField) { } break; + case KBN_FIELD_TYPES.UNKNOWN: + const maybeFieldType = field.esTypes?.[0]; + if (maybeFieldType && SUPPORTED_FIELD_TYPES_LIST.includes(maybeFieldType)) { + type = maybeFieldType; + } else { + type = getFieldType(field); + } + break; + default: type = getFieldType(field); break; diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 795237ef9b427..4da0244b2ec5e 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import dedent from 'dedent'; + import { ENTERPRISE_SEARCH_APP_ID, ENTERPRISE_SEARCH_CONTENT_APP_ID, @@ -210,6 +212,58 @@ export const SEARCH_RELEVANCE_PLUGIN = { SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/', }; +export const CREATE_CONNECTOR_PLUGIN = { + CLI_SNIPPET: dedent`./bin/connectors connector create + --index-name my-index + --index-language en + --from-file config.yml + `, + CONSOLE_SNIPPET: dedent`# Create an index +PUT /my-index-000001 +{ + "settings": { + "index": { + "number_of_shards": 3, + "number_of_replicas": 2 + } + } +} + +# Create an API key +POST /_security/api_key +{ + "name": "my-api-key", + "expiration": "1d", + "role_descriptors": + { + "role-a": { + "cluster": ["all"], + "indices": [ + { + "names": ["index-a*"], + "privileges": ["read"] + } + ] + }, + "role-b": { + "cluster": ["all"], + "indices": [ + { + "names": ["index-b*"], + "privileges": ["all"] + }] + } + }, "metadata": + { "application": "my-application", + "environment": { + "level": 1, + "trusted": true, + "tags": ["dev", "staging"] + } + } + }`, +}; + export const LICENSED_SUPPORT_URL = 'https://support.elastic.co'; export const JSON_HEADER = { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts index be8e23bdca1c5..3593a7b123533 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; interface AddConnectorValue { @@ -20,11 +20,17 @@ export interface AddConnectorApiLogicArgs { language: string | null; name: string; serviceType?: string; + // Without a proper refactoring there is no good way to chain actions. + // This prop is simply passed back with the result to let listeners + // know what was the intent of the request. And call the next action + // accordingly. + uiFlags?: Record; } export interface AddConnectorApiLogicResponse { id: string; indexName: string; + uiFlags?: Record; } export const addConnector = async ({ @@ -34,6 +40,7 @@ export const addConnector = async ({ isNative, language, serviceType, + uiFlags, }: AddConnectorApiLogicArgs): Promise => { const route = '/internal/enterprise_search/connectors'; @@ -54,7 +61,12 @@ export const addConnector = async ({ return { id: result.id, indexName: result.index_name, + uiFlags, }; }; export const AddConnectorApiLogic = createApiLogic(['add_connector_api_logic'], addConnector); +export type AddConnectorApiLogicActions = Actions< + AddConnectorApiLogicArgs, + AddConnectorApiLogicResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts index 21edf734bc230..449d3f6628648 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts @@ -5,13 +5,15 @@ * 2.0. */ -import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; export interface GenerateConfigApiArgs { connectorId: string; } +export type GenerateConfigApiActions = Actions; + export const generateConnectorConfig = async ({ connectorId }: GenerateConfigApiArgs) => { const route = `/internal/enterprise_search/connectors/${connectorId}/generate_config`; return await HttpLogic.values.http.post(route); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts index 5583c8c8e22e4..8d2ee0ee87aa3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts @@ -4,23 +4,38 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; export interface GenerateConnectorNamesApiArgs { + connectorName?: string; connectorType?: string; } +export interface GenerateConnectorNamesApiResponse { + apiKeyName: string; + connectorName: string; + indexName: string; +} + export const generateConnectorNames = async ( - { connectorType }: GenerateConnectorNamesApiArgs = { connectorType: 'custom' } + { connectorType, connectorName }: GenerateConnectorNamesApiArgs = { connectorType: 'custom' } ) => { + if (connectorType === '') { + connectorType = 'custom'; + } const route = `/internal/enterprise_search/connectors/generate_connector_name`; return await HttpLogic.values.http.post(route, { - body: JSON.stringify({ connectorType }), + body: JSON.stringify({ connectorName, connectorType }), }); }; export const GenerateConnectorNamesApiLogic = createApiLogic( - ['generate_config_api_logic'], + ['generate_connector_names_api_logic'], generateConnectorNames ); + +export type GenerateConnectorNamesApiLogicActions = Actions< + GenerateConnectorNamesApiArgs, + GenerateConnectorNamesApiResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx index bb34d652ee74d..ed28ba575d824 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx @@ -12,13 +12,15 @@ import { i18n } from '@kbn/i18n'; export interface GenerateConfigButtonProps { connectorId: string; + disabled?: boolean; generateConfiguration: (params: { connectorId: string }) => void; isGenerateLoading: boolean; } export const GenerateConfigButton: React.FC = ({ connectorId, + disabled, generateConfiguration, - isGenerateLoading, + isGenerateLoading = false, }) => { return ( @@ -26,6 +28,7 @@ export const GenerateConfigButton: React.FC = ({ void; + generateApiKey?: () => void; isGenerateLoading: boolean; } @@ -93,7 +93,7 @@ export const GeneratedConfigFields: React.FC = ({ }; const onConfirm = () => { - generateApiKey(); + if (generateApiKey) generateApiKey(); setIsModalVisible(false); }; @@ -222,16 +222,18 @@ export const GeneratedConfigFields: React.FC = ({ {apiKey?.encoded} - - - + {generateApiKey && ( + + + + )} = ({ ) : ( - - - + generateApiKey && ( + + + + ) )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx index 2c20902793093..e3bd0e867af3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx @@ -61,6 +61,22 @@ export const ConnectorDeployment: React.FC = () => { Record >('search:connector-ui-options', {}); + useEffect(() => { + if (connectorId && connector && connector.api_key_id) { + getApiKeyById(connector.api_key_id); + } + }, [connector, connectorId]); + + const selectDeploymentMethod = (deploymentMethod: 'docker' | 'source') => { + if (connector) { + setSelectedDeploymentMethod(deploymentMethod); + setConnectorUiOptions({ + ...connectorUiOptions, + [connector.id]: { deploymentMethod }, + }); + } + }; + useEffect(() => { if (connectorUiOptions && connectorId && connectorUiOptions[connectorId]) { setSelectedDeploymentMethod(connectorUiOptions[connectorId].deploymentMethod); @@ -68,25 +84,10 @@ export const ConnectorDeployment: React.FC = () => { selectDeploymentMethod('docker'); } }, [connectorUiOptions, connectorId]); - - useEffect(() => { - if (connectorId && connector && connector.api_key_id) { - getApiKeyById(connector.api_key_id); - } - }, [connector, connectorId]); - if (!connector || connector.is_native) { return <>; } - const selectDeploymentMethod = (deploymentMethod: 'docker' | 'source') => { - setSelectedDeploymentMethod(deploymentMethod); - setConnectorUiOptions({ - ...connectorUiOptions, - [connector.id]: { deploymentMethod }, - }); - }; - const hasApiKey = !!(connector.api_key_id ?? generatedData?.apiKey); const isWaitingForConnector = !connector.status || connector.status === ConnectorStatus.CREATED; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts index 09c2c8db48e03..13f3cc0b30369 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts @@ -10,15 +10,12 @@ import { kea, MakeLogicType } from 'kea'; import { Connector } from '@kbn/search-connectors'; import { HttpError, Status } from '../../../../../common/types/api'; -import { Actions } from '../../../shared/api_logic/create_api_logic'; import { - GenerateConfigApiArgs, + GenerateConfigApiActions, GenerateConfigApiLogic, } from '../../api/connector/generate_connector_config_api_logic'; import { APIKeyResponse } from '../../api/generate_api_key/generate_api_key_logic'; -type GenerateConfigApiActions = Actions; - export interface DeploymentLogicValues { generateConfigurationError: HttpError; generateConfigurationStatus: Status; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx index a29f6c540b7ce..c12dd8036b6b9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx @@ -44,8 +44,8 @@ import { ConnectorStats } from './connector_stats'; import { ConnectorsLogic } from './connectors_logic'; import { ConnectorsTable } from './connectors_table'; import { CrawlerEmptyState } from './crawler_empty_state'; +import { CreateConnector } from './create_connector'; import { DeleteConnectorModal } from './delete_connector_modal'; -import { SelectConnector } from './select_connector/select_connector'; export const connectorsBreadcrumbs = [ i18n.translate('xpack.enterpriseSearch.content.connectors.breadcrumb', { @@ -81,7 +81,7 @@ export const Connectors: React.FC = ({ isCrawler }) => { }, [searchParams.from, searchParams.size, searchQuery, isCrawler]); return !isLoading && isEmpty && !isCrawler ? ( - + ) : ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx index dc5ed0342c3be..9020a1d165168 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx @@ -13,23 +13,27 @@ import { CONNECTORS_PATH, NEW_INDEX_SELECT_CONNECTOR_PATH, NEW_CONNECTOR_PATH, + NEW_CONNECTOR_FLOW_PATH, CONNECTOR_DETAIL_PATH, } from '../../routes'; import { ConnectorDetailRouter } from '../connector_detail/connector_detail_router'; import { NewSearchIndexPage } from '../new_index/new_search_index_page'; import { Connectors } from './connectors'; -import { SelectConnector } from './select_connector/select_connector'; +import { CreateConnector } from './create_connector'; export const ConnectorsRouter: React.FC = () => { return ( - + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logo.svg b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logo.svg new file mode 100644 index 0000000000000..f827c8dce36eb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logos_comp.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logos_comp.png new file mode 100644 index 0000000000000..22f5ad4c31a31 Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logos_comp.png differ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx new file mode 100644 index 0000000000000..6c5505a22f81e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiBadge, + EuiFlexItem, + EuiIcon, + EuiInputPopover, + EuiSelectable, + EuiSelectableOption, + useEuiTheme, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { KibanaLogic } from '../../../../../shared/kibana'; +import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic'; +import { SelfManagePreference } from '../create_connector'; + +interface ChooseConnectorSelectableProps { + selfManaged: SelfManagePreference; +} +interface OptionData { + secondaryContent?: string; +} + +export const ChooseConnectorSelectable: React.FC = ({ + selfManaged, +}) => { + const { euiTheme } = useEuiTheme(); + const [isOpen, setIsOpen] = useState(false); + const [selectableOptions, selectableSetOptions] = useState< + Array> + >([]); + const { connectorTypes } = useValues(KibanaLogic); + const allConnectors = useMemo( + () => connectorTypes.sort((a, b) => a.name.localeCompare(b.name)), + [connectorTypes] + ); + const { selectedConnector } = useValues(NewConnectorLogic); + const { setSelectedConnector } = useActions(NewConnectorLogic); + + const getInitialOptions = () => { + return allConnectors.map((connector, key) => { + const append: JSX.Element[] = []; + if (connector.isTechPreview) { + append.push( + + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel', + { defaultMessage: 'Tech preview' } + )} + + ); + } + if (connector.isBeta) { + append.push( + + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel', + { + defaultMessage: 'Beta', + } + )} + + ); + } + if (selfManaged === 'native' && !connector.isNative) { + append.push( + + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel', + { + defaultMessage: 'Self managed', + } + )} + + ); + } + + return { + append, + key: key.toString(), + label: connector.name, + prepend: , + }; + }); + }; + + const initialOptions = getInitialOptions(); + + useEffect(() => { + selectableSetOptions(initialOptions); + }, [selfManaged]); + const [searchValue, setSearchValue] = useState(''); + + const openPopover = useCallback(() => { + setIsOpen(true); + }, []); + const closePopover = useCallback(() => { + setIsOpen(false); + }, []); + + return ( + + { + selectableSetOptions(newOptions); + closePopover(); + if (changedOption.checked === 'on') { + const keySelected = Number(changedOption.key); + setSelectedConnector(allConnectors[keySelected]); + setSearchValue(allConnectors[keySelected].name); + } else { + setSelectedConnector(null); + setSearchValue(''); + } + }} + listProps={{ + isVirtualized: true, + rowHeight: Number(euiTheme.base * 3), + showIcons: false, + }} + singleSelection + searchable + searchProps={{ + fullWidth: true, + isClearable: true, + onChange: (value) => { + if (value !== selectedConnector?.name) { + setSearchValue(value); + } + }, + onClick: openPopover, + onFocus: openPopover, + placeholder: i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text', + { defaultMessage: 'Choose a data source' } + ), + value: searchValue, + }} + > + {(list, search) => ( + + {list} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/connector_description_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/connector_description_popover.tsx new file mode 100644 index 0000000000000..b19a5ac8ddba5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/connector_description_popover.tsx @@ -0,0 +1,166 @@ +/* + * 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, { useState } from 'react'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import connectorLogo from '../../../../../../assets/images/connector_logo_network_drive_version.svg'; + +const nativePopoverPanels = [ + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.native.chooseADataSourceLabel', + { defaultMessage: 'Choose a data source you would like to sync' } + ), + icons: [], + id: 'native-choose-source', + }, + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.native.configureConnectorLabel', + { defaultMessage: 'Configure your connector using our Kibana UI' } + ), + icons: [, ], + id: 'native-configure-connector', + }, +]; + +const connectorClientPopoverPanels = [ + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.chooseADataSourceLabel', + { defaultMessage: 'Choose a data source you would like to sync' } + ), + icons: [], + id: 'client-choose-source', + }, + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.configureConnectorLabel', + { + defaultMessage: + 'Deploy connector code on your own infrastructure by running from source or using Docker', + } + ), + icons: [ + , + , + , + ], + id: 'client-deploy', + }, + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.enterDetailsLabel', + { + defaultMessage: 'Enter access and connection details for your data source', + } + ), + icons: [ + , + , + , + , + , + ], + id: 'client-configure-connector', + }, +]; + +export interface ConnectorDescriptionPopoverProps { + isDisabled: boolean; + isNative: boolean; +} + +export const ConnectorDescriptionPopover: React.FC = ({ + isNative, + isDisabled, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const panels = isNative ? nativePopoverPanels : connectorClientPopoverPanels; + return ( + setIsPopoverOpen(!isPopoverOpen)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => { + setIsPopoverOpen(false); + }} + > + + {isDisabled && ( + + + + + + )} + + + {panels.map((panel) => { + return ( + + + + + {panel.icons.map((icon, index) => ( + + {icon} + + ))} + + + + +

{panel.description}

+
+
+
+
+ ); + })} +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration.tsx new file mode 100644 index 0000000000000..13273266a2068 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration.tsx @@ -0,0 +1,114 @@ +/* + * 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, { useState } from 'react'; + +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + useGeneratedHtmlId, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { SelfManagePreference } from '../create_connector'; + +import { ManualConfigurationFlyout } from './manual_configuration_flyout'; + +export interface ManualConfigurationProps { + isDisabled: boolean; + selfManagePreference: SelfManagePreference; +} + +export const ManualConfiguration: React.FC = ({ + isDisabled, + selfManagePreference, +}) => { + const [isPopoverOpen, setPopover] = useState(false); + const splitButtonPopoverId = useGeneratedHtmlId({ + prefix: 'splitButtonPopover', + }); + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [flyoutContent, setFlyoutContent] = useState<'manual_config' | 'client'>(); + + const items = [ + { + setFlyoutContent('manual_config'); + setIsFlyoutVisible(true); + closePopover(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.manageAttachedIndexContextMenuItemLabel', + { defaultMessage: 'Manual configuration' } + )} + , + { + setFlyoutContent('client'); + setIsFlyoutVisible(true); + closePopover(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.scheduleASyncContextMenuItemLabel', + { + defaultMessage: 'Try with CLI', + } + )} + , + ]; + + return ( + <> + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + {isFlyoutVisible && ( + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration_flyout.tsx new file mode 100644 index 0000000000000..6fc80ec3a81f1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration_flyout.tsx @@ -0,0 +1,228 @@ +/* + * 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 { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiCode, + EuiCodeBlock, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFormRow, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FormattedMessage } from '@kbn/i18n-react'; + +import { CREATE_CONNECTOR_PLUGIN } from '../../../../../../../common/constants'; +import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic'; + +import { SelfManagePreference } from '../create_connector'; + +const CLI_LABEL = i18n.translate( + 'xpack.enterpriseSearch.createConnector.manualConfiguration.cliLabel', + { + defaultMessage: 'CLI', + } +); + +export interface ManualConfigurationFlyoutProps { + flyoutContent: string | undefined; + selfManagePreference: SelfManagePreference; + setIsFlyoutVisible: (value: boolean) => void; +} +export const ManualConfigurationFlyout: React.FC = ({ + flyoutContent, + selfManagePreference, + setIsFlyoutVisible, +}) => { + const simpleFlyoutTitleId = useGeneratedHtmlId({ + prefix: 'simpleFlyoutTitle', + }); + + const { connectorName } = useValues(NewConnectorLogic); + const { setRawName, createConnector } = useActions(NewConnectorLogic); + + return ( + setIsFlyoutVisible(false)} + aria-labelledby={simpleFlyoutTitleId} + size="s" + > + {flyoutContent === 'manual_config' && ( + <> + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.manualConfiguration.h2.cliLabel', + { + defaultMessage: 'Manual configuration', + } + )} +

+
+ + +

+ + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.manualConfiguration.generateConfigLinkLabel', + { + defaultMessage: 'Generate configuration', + } + )} + + ), + }} + /> +

+
+
+ + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.manualConfiguration.connectorName', + { + defaultMessage: 'Connector', + } + )} +

+
+ + + { + setRawName(e.target.value); + }} + /> + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.manualConfiguration.p.connectorNameDescription', + { + defaultMessage: + 'You will be redirected to the connector page to configure the rest of your connector', + } + )} +

+
+
+
+
+ + + + setIsFlyoutVisible(false)} + flush="left" + > + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.flyoutManualConfigContent.closeButtonEmptyLabel', + { defaultMessage: 'Close' } + )} + + + + { + createConnector({ + isSelfManaged: selfManagePreference === 'selfManaged', + shouldGenerateAfterCreate: false, + shouldNavigateToConnectorAfterCreate: true, + }); + }} + fill + > + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.flyoutManualConfigContent.saveConfigurationButtonLabel', + { defaultMessage: 'Save configuration' } + )} + + + + + + )} + {flyoutContent === 'client' && ( + <> + + +

{CLI_LABEL}

+
+
+ + +

+ + {CLI_LABEL} + + ), + myIndex: my-index, + }} + /> +

+
+ + + {CREATE_CONNECTOR_PLUGIN.CLI_SNIPPET} + +
+ + )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx new file mode 100644 index 0000000000000..8644cd72f53d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx @@ -0,0 +1,122 @@ +/* + * 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, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, + EuiText, + EuiButton, + EuiProgress, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ConnectorConfigurationComponent, ConnectorStatus } from '@kbn/search-connectors'; + +import { Status } from '../../../../../../common/types/api'; + +import * as Constants from '../../../../shared/constants'; +import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic'; +import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic'; + +interface ConfigurationStepProps { + setCurrentStep: Function; + title: string; +} + +export const ConfigurationStep: React.FC = ({ title, setCurrentStep }) => { + const { connector } = useValues(ConnectorViewLogic); + const { updateConnectorConfiguration } = useActions(ConnectorViewLogic); + const { status } = useValues(ConnectorConfigurationApiLogic); + const isSyncing = false; + + const isNextStepEnabled = + connector?.status === ConnectorStatus.CONNECTED || + connector?.status === ConnectorStatus.CONFIGURED; + + useEffect(() => { + setTimeout(() => { + window.scrollTo({ + behavior: 'smooth', + top: 0, + }); + }, 100); + }, []); + + if (!connector) return null; + + return ( + <> + + + + +

{title}

+
+ + { + updateConnectorConfiguration({ + configuration: config, + connectorId: connector.id, + }); + }} + /> + + {isSyncing && ( + + )} +
+
+ + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.configurationStep.h4.finishUpLabel', + { + defaultMessage: 'Finish up', + } + )} +

+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.configurationStep.p.description', + { + defaultMessage: + 'You can manually sync your data, schedule a recurring sync or manage your domains.', + } + )} +

+
+ + setCurrentStep('finish')} + fill + > + {Constants.NEXT_BUTTON_LABEL} + +
+
+
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/create_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/create_connector.tsx new file mode 100644 index 0000000000000..e8cef81662096 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/create_connector.tsx @@ -0,0 +1,265 @@ +/* + * 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, { useEffect, useState } from 'react'; + +import { css } from '@emotion/react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiPanel, + EuiSpacer, + EuiSteps, + EuiSuperSelect, + EuiText, + useEuiTheme, +} from '@elastic/eui'; + +import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; +import { i18n } from '@kbn/i18n'; + +import { AddConnectorApiLogic } from '../../../api/connector/add_connector_api_logic'; +import { EnterpriseSearchContentPageTemplate } from '../../layout'; +import { NewConnectorLogic } from '../../new_index/method_connector/new_connector_logic'; +import { errorToText } from '../../new_index/utils/error_to_text'; +import { connectorsBreadcrumbs } from '../connectors'; + +import { generateStepState } from '../utils/generate_step_state'; + +import connectorsBackgroundImage from './assets/connector_logos_comp.png'; + +import { ConfigurationStep } from './configuration_step'; +import { DeploymentStep } from './deployment_step'; +import { FinishUpStep } from './finish_up_step'; +import { StartStep } from './start_step'; + +export type ConnectorCreationSteps = 'start' | 'deployment' | 'configure' | 'finish'; +export type SelfManagePreference = 'native' | 'selfManaged'; +export const CreateConnector: React.FC = () => { + const { error } = useValues(AddConnectorApiLogic); + const { euiTheme } = useEuiTheme(); + const [selfManagePreference, setSelfManagePreference] = useState('native'); + + const { selectedConnector, currentStep } = useValues(NewConnectorLogic); + const { setCurrentStep } = useActions(NewConnectorLogic); + const stepStates = generateStepState(currentStep); + + useEffect(() => { + // TODO: separate this to ability and preference + if (!selectedConnector?.isNative || !selfManagePreference) { + setSelfManagePreference('selfManaged'); + } else { + setSelfManagePreference('native'); + } + }, [selectedConnector]); + + const getSteps = (selfManaged: boolean): EuiContainedStepProps[] => { + return [ + { + children: null, + status: stepStates.start, + title: i18n.translate('xpack.enterpriseSearch.createConnector.startStep.startLabel', { + defaultMessage: 'Start', + }), + }, + ...(selfManaged + ? [ + { + children: null, + status: stepStates.deployment, + title: i18n.translate( + 'xpack.enterpriseSearch.createConnector.deploymentStep.deploymentLabel', + { defaultMessage: 'Deployment' } + ), + }, + ] + : []), + { + children: null, + status: stepStates.configure, + title: i18n.translate( + 'xpack.enterpriseSearch.createConnector.configurationStep.configurationLabel', + { defaultMessage: 'Configuration' } + ), + }, + + { + children: null, + status: stepStates.finish, + title: i18n.translate('xpack.enterpriseSearch.createConnector.finishUpStep.finishUpLabel', { + defaultMessage: 'Finish up', + }), + }, + ]; + }; + + const stepContent: Record<'start' | 'deployment' | 'configure' | 'finish', React.ReactNode> = { + configure: ( + + ), + deployment: , + finish: ( + + ), + start: ( + { + setSelfManagePreference(preference); + }} + error={errorToText(error)} + /> + ), + }; + + return ( + + + {/* Col 1 */} + + + css` + .euiStep__content { + padding-block-end: ${euiTheme.size.xs}; + } + `} + /> + + {selectedConnector?.docsUrl && selectedConnector?.docsUrl !== '' && ( + <> + +

+ + {'Elastic '} + {selectedConnector?.name} + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.connectorDocsLinkLabel', + { defaultMessage: ' connector reference' } + )} + +

+
+ + + )} + {currentStep !== 'start' && ( + <> + + + + {selectedConnector?.name} + + ), + value: 'item1', + }, + ]} + /> + + + + {selfManagePreference + ? i18n.translate( + 'xpack.enterpriseSearch.createConnector.badgeType.selfManaged', + { + defaultMessage: 'Self managed', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.createConnector.badgeType.ElasticManaged', + { + defaultMessage: 'Elastic managed', + } + )} + + + )} +
+
+ {/* Col 2 */} + {stepContent[currentStep]} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/deployment_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/deployment_step.tsx new file mode 100644 index 0000000000000..6e5245f072b4b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/deployment_step.tsx @@ -0,0 +1,83 @@ +/* + * 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, { useEffect } from 'react'; + +import { useValues } from 'kea'; + +import { EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiButton, EuiFlexGroup } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ConnectorStatus } from '@kbn/search-connectors'; + +import * as Constants from '../../../../shared/constants'; +import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic'; +import { ConnectorDeployment } from '../../connector_detail/deployment'; + +interface DeploymentStepProps { + setCurrentStep: Function; +} + +export const DeploymentStep: React.FC = ({ setCurrentStep }) => { + const { connector } = useValues(ConnectorViewLogic); + const isNextStepEnabled = + connector && !(!connector.status || connector.status === ConnectorStatus.CREATED); + + useEffect(() => { + setTimeout(() => { + window.scrollTo({ + behavior: 'smooth', + top: 0, + }); + }, 100); + }, []); + return ( + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.DeploymentStep.Configuration.title', + { + defaultMessage: 'Configuration', + } + )} +

+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.DeploymentStep.Configuration.description', + { + defaultMessage: 'Now configure your Elastic crawler and sync the data.', + } + )} +

+
+ + setCurrentStep('configure')} + fill + disabled={!isNextStepEnabled} + > + {Constants.NEXT_BUTTON_LABEL} + +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/finish_up_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/finish_up_step.tsx new file mode 100644 index 0000000000000..28d5387ae4b70 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/finish_up_step.tsx @@ -0,0 +1,348 @@ +/* + * 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, { useEffect, useState } from 'react'; + +import { css } from '@emotion/react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiTitle, + useEuiTheme, + EuiProgress, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +import { APPLICATIONS_PLUGIN } from '../../../../../../common/constants'; + +import { KibanaDeps } from '../../../../../../common/types'; + +import { PLAYGROUND_PATH } from '../../../../applications/routes'; +import { generateEncodedPath } from '../../../../shared/encode_path_params'; +import { KibanaLogic } from '../../../../shared/kibana'; + +import { CONNECTOR_DETAIL_TAB_PATH } from '../../../routes'; +import { ConnectorDetailTabId } from '../../connector_detail/connector_detail'; +import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic'; +import { IndexViewLogic } from '../../search_index/index_view_logic'; +import { SyncsLogic } from '../../shared/header_actions/syncs_logic'; + +import connectorLogo from './assets/connector_logo.svg'; + +interface FinishUpStepProps { + title: string; +} + +export const FinishUpStep: React.FC = ({ title }) => { + const { euiTheme } = useEuiTheme(); + const { + services: { discover }, + } = useKibana(); + const [showNext, setShowNext] = useState(false); + + const { isWaitingForSync, isSyncing: isSyncingProp } = useValues(IndexViewLogic); + const { connector } = useValues(ConnectorViewLogic); + const { startSync } = useActions(SyncsLogic); + + const isSyncing = isWaitingForSync || isSyncingProp; + useEffect(() => { + setTimeout(() => { + window.scrollTo({ + behavior: 'smooth', + top: 0, + }); + }, 100); + }, []); + return ( + <> + + + + +

{title}

+
+ + {isSyncing && ( + <> + + + + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.syncingDataTextLabel', + { + defaultMessage: 'Syncing data', + } + )} + + + + + { + setShowNext(true); + }} + /> + + + )} + + + + } + titleSize="s" + title={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.chatWithYourDataLabel', + { defaultMessage: 'Chat with your data' } + )} + description={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.chatWithYourDataDescriptionl', + { + defaultMessage: + 'Combine your data with the power of LLMs for retrieval augmented generation (RAG)', + } + )} + footer={ + showNext ? ( + { + if (connector) { + KibanaLogic.values.navigateToUrl( + `${APPLICATIONS_PLUGIN.URL}${PLAYGROUND_PATH}?default-index=${connector.index_name}`, + { shouldNotCreateHref: true } + ); + } + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.startSearchPlaygroundButtonLabel', + { defaultMessage: 'Start Search Playground' } + )} + + ) : ( + { + startSync(connector); + setShowNext(true); + }} + > + {isSyncing ? 'Syncing data' : 'First sync data'} + + ) + } + /> + + + } + titleSize="s" + title={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.exploreYourDataLabel', + { defaultMessage: 'Explore your data' } + )} + description={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.exploreYourDataDescription', + { + defaultMessage: + 'See your connector documents or make a data view to explore them', + } + )} + footer={ + showNext ? ( + { + discover.locator?.navigate({ + dataViewSpec: { + title: connector?.name, + }, + indexPattern: connector?.index_name, + title: connector?.name, + }); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.viewInDiscoverButtonLabel', + { defaultMessage: 'View in Discover' } + )} + + ) : ( + { + startSync(connector); + setShowNext(true); + }} + > + {isSyncing ? 'Syncing data' : 'First sync data'} + + ) + } + /> + + + } + titleSize="s" + title={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.manageYourConnectorLabel', + { defaultMessage: 'Manage your connector' } + )} + description={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.manageYourConnectorDescription', + { + defaultMessage: + 'Now you can manage your connector, schedule a sync and much more', + } + )} + footer={ + + + { + if (connector) { + KibanaLogic.values.navigateToUrl( + generateEncodedPath(CONNECTOR_DETAIL_TAB_PATH, { + connectorId: connector.id, + tabId: ConnectorDetailTabId.CONFIGURATION, + }) + ); + } + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.manageConnectorButtonLabel', + { defaultMessage: 'Manage connector' } + )} + + + + } + /> + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.h3.queryYourDataLabel', + { + defaultMessage: 'Query your data', + } + )} +

+
+ + + + css` + margin-top: ${euiTheme.size.xs}; + `} + size="m" + type="visVega" + /> + } + title={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.queryWithLanguageClientsLabel', + { defaultMessage: 'Query with language clients' } + )} + titleSize="xs" + description={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.queryWithLanguageClientsLDescription', + { + defaultMessage: + 'Use your favorite language client to query your data in your app', + } + )} + onClick={() => {}} + display="subdued" + /> + + + css` + margin-top: ${euiTheme.size.xs}; + `} + size="m" + type="console" + /> + } + title={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.devToolsLabel', + { defaultMessage: 'Dev tools' } + )} + titleSize="xs" + description={i18n.translate( + 'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.devToolsDescription', + { + defaultMessage: + 'Tools for interacting with your data, such as console, profiler, Grok debugger and more', + } + )} + onClick={() => {}} + display="subdued" + /> + + +
+
+
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/index.ts new file mode 100644 index 0000000000000..f3992cbcf9fc9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CreateConnector } from './create_connector'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/start_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/start_step.tsx new file mode 100644 index 0000000000000..633ea8f58d25c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/start_step.tsx @@ -0,0 +1,340 @@ +/* + * 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, { ChangeEvent } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, + EuiRadio, + EuiSpacer, + EuiText, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import * as Constants from '../../../../shared/constants'; +import { GeneratedConfigFields } from '../../connector_detail/components/generated_config_fields'; + +import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic'; +import { NewConnectorLogic } from '../../new_index/method_connector/new_connector_logic'; + +import { ChooseConnectorSelectable } from './components/choose_connector_selectable'; +import { ConnectorDescriptionPopover } from './components/connector_description_popover'; +import { ManualConfiguration } from './components/manual_configuration'; +import { SelfManagePreference } from './create_connector'; + +interface StartStepProps { + error?: string | React.ReactNode; + onSelfManagePreferenceChange(preference: SelfManagePreference): void; + selfManagePreference: SelfManagePreference; + setCurrentStep: Function; + title: string; +} + +export const StartStep: React.FC = ({ + title, + selfManagePreference, + setCurrentStep, + onSelfManagePreferenceChange, + error, +}) => { + const elasticManagedRadioButtonId = useGeneratedHtmlId({ prefix: 'elasticManagedRadioButton' }); + const selfManagedRadioButtonId = useGeneratedHtmlId({ prefix: 'selfManagedRadioButton' }); + + const { + rawName, + canConfigureConnector, + selectedConnector, + generatedConfigData, + isGenerateLoading, + isCreateLoading, + } = useValues(NewConnectorLogic); + const { setRawName, createConnector, generateConnectorName } = useActions(NewConnectorLogic); + const { connector } = useValues(ConnectorViewLogic); + + const handleNameChange = (e: ChangeEvent) => { + setRawName(e.target.value); + }; + + return ( + + + + + +

{title}

+
+ + + + + + + + + + { + if (selectedConnector) { + generateConnectorName({ + connectorName: rawName, + connectorType: selectedConnector.serviceType, + }); + } + }} + /> + + + + + + + + + +
+
+ {/* Set up */} + + + +

+ {i18n.translate('xpack.enterpriseSearch.createConnector.startStep.h4.setUpLabel', { + defaultMessage: 'Set up', + })} +

+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.startStep.p.whereDoYouWantLabel', + { + defaultMessage: + 'Where do you want to store the connector and how do you want to manage it?', + } + )} +

+
+ + + + onSelfManagePreferenceChange('native')} + name="setUp" + /> + + + + +     + + onSelfManagePreferenceChange('selfManaged')} + name="setUp" + /> + + + + + +
+
+ {selfManagePreference === 'selfManaged' ? ( + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.startStep.h4.deploymentLabel', + { + defaultMessage: 'Deployment', + } + )} +

+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.startStep.p.youWillStartTheLabel', + { + defaultMessage: + 'You will start the process of creating a new index, API key, and a Web Crawler Connector ID manually. Optionally you can bring your own configuration as well.', + } + )} +

+
+ + { + if (selectedConnector && selectedConnector.name) { + createConnector({ + isSelfManaged: true, + }); + setCurrentStep('deployment'); + } + }} + fill + disabled={!canConfigureConnector} + isLoading={isCreateLoading || isGenerateLoading} + > + {Constants.NEXT_BUTTON_LABEL} + +
+
+ ) : ( + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.startStep.h4.configureIndexAndAPILabel', + { + defaultMessage: 'Configure index and API key', + } + )} +

+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.createConnector.startStep.p.thisProcessWillCreateLabel', + { + defaultMessage: + 'This process will create a new index, API key, and a Connector ID. Optionally you can bring your own configuration as well.', + } + )} +

+
+ + {generatedConfigData && connector ? ( + <> + + + setCurrentStep('configure')} + > + {Constants.NEXT_BUTTON_LABEL} + + + ) : ( + + + { + createConnector({ + isSelfManaged: false, + }); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.generateApiKey.button.label', + { + defaultMessage: 'Generate configuration', + } + )} + + + + + + + )} +
+
+ )} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/utils/generate_step_state.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/utils/generate_step_state.ts new file mode 100644 index 0000000000000..329ab69b5550f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/utils/generate_step_state.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiStepStatus } from '@elastic/eui'; + +type Steps = 'start' | 'configure' | 'deployment' | 'finish'; + +export const generateStepState = (currentStep: Steps): { [key in Steps]: EuiStepStatus } => { + return { + configure: + currentStep === 'start' || currentStep === 'deployment' + ? 'incomplete' + : currentStep === 'configure' + ? 'current' + : 'complete', + deployment: + currentStep === 'deployment' + ? 'current' + : currentStep === 'finish' || currentStep === 'configure' + ? 'complete' + : 'incomplete', + finish: currentStep === 'finish' ? 'current' : 'incomplete', + start: currentStep === 'start' ? 'current' : 'complete', + }; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts index 3eeb8f306dc2f..da2dcb1198800 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts @@ -7,65 +7,214 @@ import { kea, MakeLogicType } from 'kea'; +import { Connector } from '@kbn/search-connectors'; +import { ConnectorDefinition } from '@kbn/search-connectors-plugin/public'; + +import { Status } from '../../../../../../common/types/api'; import { Actions } from '../../../../shared/api_logic/create_api_logic'; +import { generateEncodedPath } from '../../../../shared/encode_path_params'; +import { KibanaLogic } from '../../../../shared/kibana'; import { AddConnectorApiLogic, + AddConnectorApiLogicActions, AddConnectorApiLogicArgs, AddConnectorApiLogicResponse, } from '../../../api/connector/add_connector_api_logic'; import { - IndexExistsApiLogic, - IndexExistsApiParams, - IndexExistsApiResponse, -} from '../../../api/index/index_exists_api_logic'; - -import { isValidIndexName } from '../../../utils/validate_index_name'; + GenerateConfigApiActions, + GenerateConfigApiLogic, +} from '../../../api/connector/generate_connector_config_api_logic'; +import { + GenerateConnectorNamesApiLogic, + GenerateConnectorNamesApiLogicActions, + GenerateConnectorNamesApiResponse, +} from '../../../api/connector/generate_connector_names_api_logic'; +import { APIKeyResponse } from '../../../api/generate_api_key/generate_api_key_logic'; -import { UNIVERSAL_LANGUAGE_VALUE } from '../constants'; -import { LanguageForOptimization } from '../types'; -import { getLanguageForOptimization } from '../utils'; +import { CONNECTOR_DETAIL_TAB_PATH } from '../../../routes'; +import { + ConnectorViewActions, + ConnectorViewLogic, +} from '../../connector_detail/connector_view_logic'; +import { ConnectorCreationSteps } from '../../connectors/create_connector/create_connector'; +import { SearchIndexTabId } from '../../search_index/search_index'; export interface NewConnectorValues { - data: IndexExistsApiResponse; - fullIndexName: string; - fullIndexNameExists: boolean; - fullIndexNameIsValid: boolean; - language: LanguageForOptimization; - languageSelectValue: string; + canConfigureConnector: boolean; + connectorId: string; + connectorName: string; + createConnectorApiStatus: Status; + currentStep: ConnectorCreationSteps; + generateConfigurationStatus: Status; + generatedConfigData: + | { + apiKey: APIKeyResponse['apiKey']; + connectorId: Connector['id']; + indexName: string; + } + | undefined; + generatedNameData: GenerateConnectorNamesApiResponse | undefined; + isCreateLoading: boolean; + isGenerateLoading: boolean; rawName: string; + selectedConnector: ConnectorDefinition | null; + shouldGenerateConfigAfterCreate: boolean; } -type NewConnectorActions = Pick< - Actions, - 'makeRequest' -> & { +type NewConnectorActions = { + generateConnectorName: GenerateConnectorNamesApiLogicActions['makeRequest']; +} & { + configurationGenerated: GenerateConfigApiActions['apiSuccess']; + generateConfiguration: GenerateConfigApiActions['makeRequest']; +} & { connectorCreated: Actions['apiSuccess']; - setLanguageSelectValue(language: string): { language: string }; + createConnector: ({ + isSelfManaged, + shouldGenerateAfterCreate, + shouldNavigateToConnectorAfterCreate, + }: { + isSelfManaged: boolean; + shouldGenerateAfterCreate?: boolean; + shouldNavigateToConnectorAfterCreate?: boolean; + }) => { + isSelfManaged: boolean; + shouldGenerateAfterCreate?: boolean; + shouldNavigateToConnectorAfterCreate?: boolean; + }; + createConnectorApi: AddConnectorApiLogicActions['makeRequest']; + fetchConnector: ConnectorViewActions['fetchConnector']; + setCurrentStep(step: ConnectorCreationSteps): { step: ConnectorCreationSteps }; setRawName(rawName: string): { rawName: string }; + setSelectedConnector(connector: ConnectorDefinition | null): { + connector: ConnectorDefinition | null; + }; }; export const NewConnectorLogic = kea>({ actions: { - setLanguageSelectValue: (language) => ({ language }), + createConnector: ({ + isSelfManaged, + shouldGenerateAfterCreate, + shouldNavigateToConnectorAfterCreate, + }) => ({ + isSelfManaged, + shouldGenerateAfterCreate, + shouldNavigateToConnectorAfterCreate, + }), + setCurrentStep: (step) => ({ step }), setRawName: (rawName) => ({ rawName }), + setSelectedConnector: (connector) => ({ connector }), }, connect: { actions: [ + GenerateConnectorNamesApiLogic, + ['makeRequest as generateConnectorName', 'apiSuccess as connectorNameGenerated'], AddConnectorApiLogic, - ['apiSuccess as connectorCreated'], - IndexExistsApiLogic, - ['makeRequest'], + ['makeRequest as createConnectorApi', 'apiSuccess as connectorCreated'], + GenerateConfigApiLogic, + ['makeRequest as generateConfiguration', 'apiSuccess as configurationGenerated'], + ConnectorViewLogic, + ['fetchConnector'], + ], + values: [ + GenerateConnectorNamesApiLogic, + ['data as generatedNameData'], + GenerateConfigApiLogic, + ['data as generatedConfigData', 'status as generateConfigurationStatus'], + AddConnectorApiLogic, + ['status as createConnectorApiStatus'], ], - values: [IndexExistsApiLogic, ['data']], }, - path: ['enterprise_search', 'content', 'new_search_index'], + listeners: ({ actions, values }) => ({ + connectorCreated: ({ id, uiFlags }) => { + if (uiFlags?.shouldNavigateToConnectorAfterCreate) { + KibanaLogic.values.navigateToUrl( + generateEncodedPath(CONNECTOR_DETAIL_TAB_PATH, { + connectorId: id, + tabId: SearchIndexTabId.CONFIGURATION, + }) + ); + } else { + actions.fetchConnector({ connectorId: id }); + if (!uiFlags || uiFlags.shouldGenerateAfterCreate) { + actions.generateConfiguration({ connectorId: id }); + } + } + }, + connectorNameGenerated: ({ connectorName }) => { + if (!values.rawName) { + actions.setRawName(connectorName); + } + }, + createConnector: ({ + isSelfManaged, + shouldGenerateAfterCreate = true, + shouldNavigateToConnectorAfterCreate = false, + }) => { + if ( + !values.rawName && + values.selectedConnector && + values.connectorName && + values.generatedNameData + ) { + // name is generated, use everything generated + actions.createConnectorApi({ + deleteExistingConnector: false, + indexName: values.connectorName, + isNative: !values.selectedConnector.isNative ? false : !isSelfManaged, + language: null, + name: values.generatedNameData.connectorName, + serviceType: values.selectedConnector.serviceType, + uiFlags: { + shouldGenerateAfterCreate, + shouldNavigateToConnectorAfterCreate, + }, + }); + } else { + if (values.generatedNameData && values.selectedConnector) { + actions.createConnectorApi({ + deleteExistingConnector: false, + indexName: values.generatedNameData.indexName, + isNative: !values.selectedConnector.isNative ? false : !isSelfManaged, + language: null, + name: values.connectorName, + serviceType: values.selectedConnector?.serviceType, + uiFlags: { + shouldGenerateAfterCreate, + shouldNavigateToConnectorAfterCreate, + }, + }); + } + } + }, + setSelectedConnector: ({ connector }) => { + if (connector) { + actions.generateConnectorName({ + connectorName: values.rawName, + connectorType: connector.serviceType, + }); + } + }, + }), + path: ['enterprise_search', 'content', 'new_search_connector'], reducers: { - languageSelectValue: [ - UNIVERSAL_LANGUAGE_VALUE, + connectorId: [ + '', { - // @ts-expect-error upgrade typescript v5.1.6 - setLanguageSelectValue: (_, { language }) => language ?? null, + connectorCreated: ( + _: NewConnectorValues['connectorId'], + { id }: { id: NewConnectorValues['connectorId'] } + ) => id, + }, + ], + currentStep: [ + 'start', + { + setCurrentStep: ( + _: NewConnectorValues['currentStep'], + { step }: { step: NewConnectorValues['currentStep'] } + ) => step, }, ], rawName: [ @@ -75,21 +224,34 @@ export const NewConnectorLogic = kea rawName, }, ], + selectedConnector: [ + null, + { + setSelectedConnector: ( + _: NewConnectorValues['selectedConnector'], + { connector }: { connector: NewConnectorValues['selectedConnector'] } + ) => connector, + }, + ], }, selectors: ({ selectors }) => ({ - fullIndexName: [() => [selectors.rawName], (name: string) => name], - fullIndexNameExists: [ - () => [selectors.data, selectors.fullIndexName], - (data: IndexExistsApiResponse | undefined, fullIndexName: string) => - data?.exists === true && data.indexName === fullIndexName, + canConfigureConnector: [ + () => [selectors.connectorName, selectors.selectedConnector], + (connectorName: string, selectedConnector: NewConnectorValues['selectedConnector']) => + (connectorName && selectedConnector?.name) ?? false, + ], + connectorName: [ + () => [selectors.rawName, selectors.generatedNameData], + (name: string, generatedName: NewConnectorValues['generatedNameData']) => + name ? name : generatedName?.connectorName ?? '', ], - fullIndexNameIsValid: [ - () => [selectors.fullIndexName], - (fullIndexName) => isValidIndexName(fullIndexName), + isCreateLoading: [ + () => [selectors.createConnectorApiStatus], + (status) => status === Status.LOADING, ], - language: [ - () => [selectors.languageSelectValue], - (languageSelectValue) => getLanguageForOptimization(languageSelectValue), + isGenerateLoading: [ + () => [selectors.generateConfigurationStatus], + (status) => status === Status.LOADING, ], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_template.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_template.tsx index 4b4aba1761450..773c81761944d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_template.tsx @@ -54,44 +54,17 @@ export const NewConnectorTemplate: React.FC = ({ type, isBeta, }) => { - const { fullIndexName, fullIndexNameExists, fullIndexNameIsValid, rawName } = - useValues(NewConnectorLogic); + const { connectorName, rawName } = useValues(NewConnectorLogic); const { setRawName } = useActions(NewConnectorLogic); const handleNameChange = (e: ChangeEvent) => { setRawName(e.target.value); if (onNameChange) { - onNameChange(fullIndexName); + onNameChange(connectorName); } }; - const formInvalid = !!error || fullIndexNameExists || !fullIndexNameIsValid; - - const formError = () => { - if (fullIndexNameExists) { - return i18n.translate( - 'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error', - { - defaultMessage: 'A connector with the name {connectorName} already exists', - values: { - connectorName: fullIndexName, - }, - } - ); - } - if (!fullIndexNameIsValid) { - return i18n.translate( - 'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error', - { - defaultMessage: '{connectorName} is an invalid connector name', - values: { - connectorName: fullIndexName, - }, - } - ); - } - return error; - }; + const formInvalid = !!error; return ( <> @@ -100,7 +73,7 @@ export const NewConnectorTemplate: React.FC = ({ id="enterprise-search-create-connector" onSubmit={(event) => { event.preventDefault(); - onSubmit(fullIndexName); + onSubmit(connectorName); }} > @@ -131,10 +104,10 @@ export const NewConnectorTemplate: React.FC = ({ } )} isInvalid={formInvalid} - error={formError()} fullWidth > = ({ {type === INGESTION_METHOD_IDS.CONNECTOR && ( - + {i18n.translate( 'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText', { @@ -182,6 +159,7 @@ export const NewConnectorTemplate: React.FC = ({ history.back()} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ingestion_card/ingestion_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ingestion_card/ingestion_card.tsx index 94bbc515f92bd..f935ea6803c69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ingestion_card/ingestion_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ingestion_card/ingestion_card.tsx @@ -20,7 +20,7 @@ import { import { i18n } from '@kbn/i18n'; -import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { EuiButtonTo } from '../../../../shared/react_router_helpers'; interface IngestionCardProps { buttonIcon: IconType; @@ -78,15 +78,25 @@ export const IngestionCard: React.FC = ({ } footer={ onClick ? ( - + {buttonLabel} ) : ( - - - {buttonLabel} - - + + {buttonLabel} + ) } /> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts index 6be30af4e986b..092b60bf7666f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts @@ -21,6 +21,7 @@ export const NEW_ES_INDEX_PATH = `${NEW_INDEX_PATH}/elasticsearch`; export const NEW_DIRECT_UPLOAD_PATH = `${NEW_INDEX_PATH}/upload`; export const NEW_INDEX_SELECT_CONNECTOR_PATH = `${CONNECTORS_PATH}/select_connector`; export const NEW_CONNECTOR_PATH = `${CONNECTORS_PATH}/new_connector`; +export const NEW_CONNECTOR_FLOW_PATH = `${CONNECTORS_PATH}/new_connector_flow`; export const NEW_CRAWLER_PATH = `${CRAWLERS_PATH}/new_crawler`; export const NEW_INDEX_SELECT_CONNECTOR_NATIVE_PATH = `${CONNECTORS_PATH}/select_connector?filter=native`; export const NEW_INDEX_SELECT_CONNECTOR_CLIENTS_PATH = `${CONNECTORS_PATH}/select_connector?filter=connector_clients`; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/basic_setup_form.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/basic_setup_form.tsx index 0964f2909d85d..42a20a44dd06e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/basic_setup_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/basic_setup_form.tsx @@ -117,6 +117,7 @@ export const BasicSetupForm: React.FC = ({ 'data-test-subj': 'create-api-key-expires-days-radio', }, ]} + name="create-api-key-expires-group" idSelected={expires === null ? 'never' : 'days'} onChange={(id) => onChangeExpires(id === 'never' ? null : DEFAULT_EXPIRES_VALUE)} data-test-subj="create-api-key-expires-radio" diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/create_api_key_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/create_api_key_flyout.tsx index fe298fbd98f4b..38217df269fd1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/create_api_key_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/create_api_key_flyout.tsx @@ -32,6 +32,7 @@ import { EuiSwitchEvent, EuiText, EuiTitle, + useGeneratedHtmlId, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -161,6 +162,8 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose const apiKeyRef = useRef(null); + const uniqueId = useGeneratedHtmlId(); + useEffect(() => { if (createdApiKey && apiKeyRef) { apiKeyRef.current?.scrollIntoView(); @@ -178,10 +181,11 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose css={css` max-width: calc(${euiTheme.size.xxxxl} * 10); `} + aria-labelledby={`${uniqueId}-header`} > -

+

{i18n.translate('xpack.enterpriseSearch.apiKey.flyoutTitle', { defaultMessage: 'Create an API key', })} @@ -239,6 +243,8 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose id="apiKey.setup" paddingSize="l" initialIsOpen + aria-labelledby={`${uniqueId}-setupHeader`} + role="region" buttonContent={
@@ -247,7 +253,7 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose -

+

{i18n.translate('xpack.enterpriseSearch.apiKey.setup.title', { defaultMessage: 'Setup', })} @@ -283,6 +289,8 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose @@ -291,7 +299,7 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose -

+

{i18n.translate('xpack.enterpriseSearch.apiKey.privileges.title', { defaultMessage: 'Security Privileges', })} @@ -338,6 +346,8 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose @@ -346,7 +356,7 @@ export const CreateApiKeyFlyout: React.FC = ({ onClose -

+

{i18n.translate('xpack.enterpriseSearch.apiKey.metadata.title', { defaultMessage: 'Metadata', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts index f163158462f0d..fc9860e202130 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts @@ -49,6 +49,10 @@ export const BACK_BUTTON_LABEL = i18n.translate('xpack.enterpriseSearch.actions. defaultMessage: 'Back', }); +export const NEXT_BUTTON_LABEL = i18n.translate('xpack.enterpriseSearch.actions.nextButtonLabel', { + defaultMessage: 'Next', +}); + export const CLOSE_BUTTON_LABEL = i18n.translate( 'xpack.enterpriseSearch.actions.closeButtonLabel', { defaultMessage: 'Close' } diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts index f6c209707a8f7..56f849c551400 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts @@ -16,24 +16,51 @@ import { indexOrAliasExists } from '../indices/exists_index'; export const generateConnectorName = async ( client: IScopedClusterClient, - connectorType: string + connectorType: string, + userConnectorName?: string ): Promise<{ apiKeyName: string; connectorName: string; indexName: string }> => { const prefix = toAlphanumeric(connectorType); if (!prefix || prefix.length === 0) { - throw new Error('Connector type is required'); + throw new Error('Connector type or connectorName is required'); } - for (let i = 0; i < 20; i++) { - const connectorName = `${prefix}-${uuidv4().split('-')[1]}`; - const indexName = `connector-${connectorName}`; - - const result = await indexOrAliasExists(client, indexName); - if (!result) { + if (userConnectorName) { + let indexName = `connector-${userConnectorName}`; + const resultSameName = await indexOrAliasExists(client, indexName); + // index with same name doesn't exist + if (!resultSameName) { return { - apiKeyName: indexName, - connectorName, + apiKeyName: userConnectorName, + connectorName: userConnectorName, indexName, }; } + // if the index name already exists, we will generate until it doesn't for 20 times + for (let i = 0; i < 20; i++) { + indexName = `connector-${userConnectorName}-${uuidv4().split('-')[1].slice(0, 4)}`; + + const result = await indexOrAliasExists(client, indexName); + if (!result) { + return { + apiKeyName: indexName, + connectorName: userConnectorName, + indexName, + }; + } + } + } else { + for (let i = 0; i < 20; i++) { + const connectorName = `${prefix}-${uuidv4().split('-')[1].slice(0, 4)}`; + const indexName = `connector-${connectorName}`; + + const result = await indexOrAliasExists(client, indexName); + if (!result) { + return { + apiKeyName: indexName, + connectorName, + indexName, + }; + } + } } throw new Error(ErrorCode.GENERATE_INDEX_NAME_ERROR); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts index 21b00e82b6aa0..6108580463893 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -6,7 +6,6 @@ */ import { schema } from '@kbn/config-schema'; - import { ElasticsearchErrorDetails } from '@kbn/es-errors'; import { i18n } from '@kbn/i18n'; @@ -841,15 +840,20 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { path: '/internal/enterprise_search/connectors/generate_connector_name', validate: { body: schema.object({ + connectorName: schema.maybe(schema.string()), connectorType: schema.string(), }), }, }, elasticsearchErrorHandler(log, async (context, request, response) => { const { client } = (await context.core).elasticsearch; - const { connectorType } = request.body; + const { connectorType, connectorName } = request.body; try { - const generatedNames = await generateConnectorName(client, connectorType ?? 'custom'); + const generatedNames = await generateConnectorName( + client, + connectorType ?? 'custom', + connectorName + ); return response.ok({ body: generatedNames, headers: { 'content-type': 'application/json' }, diff --git a/x-pack/plugins/entity_manager/server/lib/entities/errors/entity_definition_update_conflict.ts b/x-pack/plugins/entity_manager/server/lib/entities/errors/entity_definition_update_conflict.ts new file mode 100644 index 0000000000000..1e85d91a7bb01 --- /dev/null +++ b/x-pack/plugins/entity_manager/server/lib/entities/errors/entity_definition_update_conflict.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class EntityDefinitionUpdateConflict extends Error { + constructor(message: string) { + super(message); + this.name = 'EntityDefinitionUpdateConflict'; + } +} diff --git a/x-pack/plugins/entity_manager/server/lib/entity_client.ts b/x-pack/plugins/entity_manager/server/lib/entity_client.ts index 710872c04eda0..1bb1322be356f 100644 --- a/x-pack/plugins/entity_manager/server/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/server/lib/entity_client.ts @@ -5,17 +5,23 @@ * 2.0. */ -import { EntityDefinition } from '@kbn/entities-schema'; +import { EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { Logger } from '@kbn/logging'; -import { installEntityDefinition } from './entities/install_entity_definition'; +import { + installEntityDefinition, + installationInProgress, + reinstallEntityDefinition, +} from './entities/install_entity_definition'; import { startTransforms } from './entities/start_transforms'; -import { findEntityDefinitions } from './entities/find_entity_definition'; +import { findEntityDefinitionById, findEntityDefinitions } from './entities/find_entity_definition'; import { uninstallEntityDefinition } from './entities/uninstall_entity_definition'; import { EntityDefinitionNotFound } from './entities/errors/entity_not_found'; import { stopTransforms } from './entities/stop_transforms'; +import { EntityDefinitionWithState } from './entities/types'; +import { EntityDefinitionUpdateConflict } from './entities/errors/entity_definition_update_conflict'; export class EntityClient { constructor( @@ -47,6 +53,50 @@ export class EntityClient { return installedDefinition; } + async updateEntityDefinition({ + id, + definitionUpdate, + }: { + id: string; + definitionUpdate: EntityDefinitionUpdate; + }) { + const definition = await findEntityDefinitionById({ + id, + soClient: this.options.soClient, + esClient: this.options.esClient, + includeState: true, + }); + + if (!definition) { + const message = `Unable to find entity definition with [${id}]`; + this.options.logger.error(message); + throw new EntityDefinitionNotFound(message); + } + + if (installationInProgress(definition)) { + const message = `Entity definition [${definition.id}] has changes in progress`; + this.options.logger.error(message); + throw new EntityDefinitionUpdateConflict(message); + } + + const shouldRestartTransforms = ( + definition as EntityDefinitionWithState + ).state.components.transforms.some((transform) => transform.running); + + const updatedDefinition = await reinstallEntityDefinition({ + definition, + definitionUpdate, + soClient: this.options.soClient, + esClient: this.options.esClient, + logger: this.options.logger, + }); + + if (shouldRestartTransforms) { + await startTransforms(this.options.esClient, updatedDefinition, this.options.logger); + } + return updatedDefinition; + } + async deleteEntityDefinition({ id, deleteData = false }: { id: string; deleteData?: boolean }) { const [definition] = await findEntityDefinitions({ id, diff --git a/x-pack/plugins/fleet/cypress.config.space_awareness.ts b/x-pack/plugins/fleet/cypress.config.space_awareness.ts new file mode 100644 index 0000000000000..6efda828e6fbc --- /dev/null +++ b/x-pack/plugins/fleet/cypress.config.space_awareness.ts @@ -0,0 +1,49 @@ +/* + * 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 { defineCypressConfig } from '@kbn/cypress-config'; + +// eslint-disable-next-line import/no-default-export +export default defineCypressConfig({ + defaultCommandTimeout: 60000, + requestTimeout: 60000, + responseTimeout: 60000, + execTimeout: 120000, + pageLoadTimeout: 120000, + + retries: { + runMode: 2, + }, + + env: { + grepFilterSpecs: false, + }, + + screenshotsFolder: '../../../target/kibana-fleet/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-fleet/cypress/videos', + viewportHeight: 900, + viewportWidth: 1440, + screenshotOnRunFailure: true, + + e2e: { + baseUrl: 'http://localhost:5601', + + experimentalRunAllSpecs: true, + experimentalMemoryManagement: true, + numTestsKeptInMemory: 3, + + specPattern: './cypress/e2e/space_awareness/**/*.cy.ts', + supportFile: './cypress/support/e2e.ts', + + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires, @kbn/imports/no_boundary_crossing + return require('./cypress/plugins')(on, config); + }, + }, +}); diff --git a/x-pack/plugins/fleet/cypress.config.ts b/x-pack/plugins/fleet/cypress.config.ts index e4a5ad96938d6..2082142e23d7f 100644 --- a/x-pack/plugins/fleet/cypress.config.ts +++ b/x-pack/plugins/fleet/cypress.config.ts @@ -39,6 +39,7 @@ export default defineCypressConfig({ specPattern: './cypress/e2e/**/*.cy.ts', supportFile: './cypress/support/e2e.ts', + excludeSpecPattern: './cypress/e2e/space_awareness/**/*.cy.ts', setupNodeEvents(on, config) { // eslint-disable-next-line @typescript-eslint/no-var-requires, @kbn/imports/no_boundary_crossing diff --git a/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts b/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts new file mode 100644 index 0000000000000..8975de388248c --- /dev/null +++ b/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ADD_AGENT_POLICY_BTN, + AGENT_POLICIES_TABLE, + AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD, + AGENT_POLICY_DETAILS_PAGE, + AGENT_POLICY_FLYOUT_CREATE_BUTTON, + AGENT_POLICY_SYSTEM_MONITORING_CHECKBOX, +} from '../../screens/fleet'; +import { login } from '../../tasks/login'; +import { createSpaces, enableSpaceAwareness } from '../../tasks/spaces'; +import { cleanupAgentPolicies } from '../../tasks/cleanup'; + +describe('Space aware policies creation', { testIsolation: false }, () => { + before(() => { + enableSpaceAwareness(); + createSpaces(); + cleanupAgentPolicies(); + cleanupAgentPolicies('test'); + login(); + }); + + beforeEach(() => { + cy.intercept('GET', /\/api\/fleet\/agent_policies/).as('getAgentPolicies'); + cy.intercept('GET', /\/internal\/fleet\/agent_policies_spaces/).as('getAgentPoliciesSpaces'); + }); + + const POLICY_NAME = `Policy 1 space test`; + const NO_AGENT_POLICIES = 'No agent policies'; + it('should allow to create an agent policy in the test space', () => { + cy.visit('/s/test/app/fleet/policies'); + + cy.getBySel(ADD_AGENT_POLICY_BTN).click(); + cy.getBySel(AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD).type(POLICY_NAME); + cy.getBySel(AGENT_POLICY_SYSTEM_MONITORING_CHECKBOX).uncheck(); + + cy.getBySel(AGENT_POLICY_FLYOUT_CREATE_BUTTON).click(); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); + + it('the created policy should not be visible in the default space', () => { + cy.visit('/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(NO_AGENT_POLICIES); + }); + + it('should allow to update that policy to belong to both test and default space', () => { + cy.visit('/s/test/app/fleet/policies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME).click(); + + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SETTINGS_TAB).click(); + cy.wait('@getAgentPoliciesSpaces'); + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SPACE_SELECTOR_COMBOBOX).click().type('default{enter}'); + + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SAVE_BUTTON).click(); + }); + + it('the policy should be visible in the test space', () => { + cy.visit('/s/test/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); + + it('the policy should be visible in the default space', () => { + cy.visit('/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index 4e1a0ac0f7e19..0bd449652a800 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -46,6 +46,8 @@ export const AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD = 'createAgentPolicyNam export const AGENT_POLICIES_FLYOUT_ADVANCED_DEFAULT_NAMESPACE_HEADER = 'defaultNamespaceHeader'; export const AGENT_POLICY_FLYOUT_CREATE_BUTTON = 'createAgentPolicyFlyoutBtn'; +export const AGENT_POLICIES_TABLE = 'agentPoliciesTable'; + export const ENROLLMENT_TOKENS = { CREATE_TOKEN_BUTTON: 'createEnrollmentTokenButton', CREATE_TOKEN_MODAL_NAME_FIELD: 'createEnrollmentTokenNameField', @@ -241,4 +243,7 @@ export const API_KEYS = { export const AGENT_POLICY_DETAILS_PAGE = { ADD_AGENT_LINK: 'addAgentLink', + SETTINGS_TAB: 'agentPolicySettingsTab', + SPACE_SELECTOR_COMBOBOX: 'spaceSelectorComboBox', + SAVE_BUTTON: 'agentPolicyDetailsSaveButton', }; diff --git a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts index 5e179bc9207f1..bad8743b66bd1 100644 --- a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts +++ b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts @@ -7,18 +7,20 @@ import { request } from './common'; -export function cleanupAgentPolicies() { - request({ url: '/api/fleet/agent_policies' }).then((response: any) => { - response.body.items - .filter((policy: any) => policy.agents === 0) - .forEach((policy: any) => { - request({ - method: 'POST', - url: '/api/fleet/agent_policies/delete', - body: { agentPolicyId: policy.id }, +export function cleanupAgentPolicies(spaceId?: string) { + request({ url: `${spaceId ? `/s/${spaceId}` : ''}/api/fleet/agent_policies` }).then( + (response: any) => { + response.body.items + .filter((policy: any) => policy.agents === 0) + .forEach((policy: any) => { + request({ + method: 'POST', + url: `${spaceId ? `/s/${spaceId}` : ''}/api/fleet/agent_policies/delete`, + body: { agentPolicyId: policy.id }, + }); }); - }); - }); + } + ); } export function unenrollAgent() { diff --git a/x-pack/plugins/fleet/cypress/tasks/common.ts b/x-pack/plugins/fleet/cypress/tasks/common.ts index 250dea07f89d1..de6e117bac4cc 100644 --- a/x-pack/plugins/fleet/cypress/tasks/common.ts +++ b/x-pack/plugins/fleet/cypress/tasks/common.ts @@ -28,6 +28,12 @@ export const COMMON_API_HEADERS = Object.freeze({ 'Elastic-Api-Version': API_VERSIONS.public.v1, }); +export const COMMON_INTERNAL_API_HEADERS = Object.freeze({ + 'kbn-xsrf': 'cypress', + 'x-elastic-internal-origin': 'fleet', + 'Elastic-Api-Version': API_VERSIONS.internal.v1, +}); + // Replaces request - adds baseline authentication + global headers export const request = ({ headers, @@ -40,6 +46,17 @@ export const request = ({ }); }; +export const internalRequest = ({ + headers, + ...options +}: Partial): Cypress.Chainable> => { + return cy.request({ + auth: API_AUTH, + headers: { ...COMMON_INTERNAL_API_HEADERS, ...headers }, + ...options, + }); +}; + /** * For all the new features tours we show in the app, this method disables them * by setting their configs in the local storage. It prevents the tours from appearing diff --git a/x-pack/plugins/fleet/cypress/tasks/spaces.ts b/x-pack/plugins/fleet/cypress/tasks/spaces.ts new file mode 100644 index 0000000000000..31c1d12a968cb --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/spaces.ts @@ -0,0 +1,38 @@ +/* + * 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 { request, internalRequest } from './common'; + +export function enableSpaceAwareness() { + return internalRequest({ + url: '/internal/fleet/enable_space_awareness', + failOnStatusCode: false, + method: 'POST', + }); +} + +export function createSpaces() { + return request({ + url: '/api/spaces/space', + failOnStatusCode: false, + method: 'POST', + body: { + id: 'test', + name: 'Test', + description: 'Test space', + color: '#aabbcc', + initials: 'TE', + disabledFeatures: [], + imageUrl: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABACAYAAABC6cT1AAAGf0lEQVRoQ+3abYydRRUH8N882xYo0IqagEVjokQJKAiKBjXExC9G/aCkGowCIghCkRcrVSSKIu/FEiqgGL6gBIlAYrAqUTH6hZgQFVEMKlQFfItWoQWhZe8z5uzMLdvbfbkLxb13d+fbvfe588x/zpn/+Z9zJpmnI81T3BaAzzfLL1h8weLzZAcWXH2eGHo7zAWLL1h8nuzAjFw9G1N6Kzq8HnuM36MR8iibF3Fv4q+7cv8yDV6K13bYq2furSP8Ag8ncr/vnSnwRViJT2GfCV7yL1yHGxLb+l3EdM9lluNEnIC9xz+f2ZL4Er6Z2DrdXN3fZwp8CU7OfDHxggle8lTLbQ1nJ/7Z7yKmey5zYGZt4h2IzR8/trRc2PDlxJPTzfVcgJ+CC0wMPOa9F6cm7up3EVM9V9386MxliVdM8GwAv6hh/awCz/w7lY25OtF5ruBz4ZLP42NYNrDAFbC3YPWuILnMAfgq3oaRQQYea/stViV+sgssvjKzLvGySeaaNVfP4d7Btokgvxj/bblgpueuF1hmWcyTCmfE3J3M1lTcv0vMswM88zR+jpw4osu6me8kzkpsfLZWzxyRuabO22buxxOJ12FxnXfWgEe83pB5sOE47BsLymzscOoi7nw2JJfZreUjiUsTyzKPZm5NvBDvSuw268AzNzV8H5/Am+qCnsAXgpgSW2Zq9cyKlksbPlTd+te4quWNieMHBfiNDdciYnwsdI/MaOaWhnMTf54J8CqNj8x8JXFIZltYu+HqlmNT8YSBsHgAPw/vxvlVV4du/s0oaxbxg0TbL/jMni0nNcVjQq7+HZfgtpbzBg342TgQ63AkmsymxBW4IjE6A+D7Vzd/fyWxIM/VuCe+HzTgZ2Jpy/kNJ2FJLmLm24mPJ/42A+Bvrxt4SISwlhsaPodH26LZB8rVA3inwwebsrixJCZzX+KMxI/7AV61eVh3DV6Mx3EOvh4kN6jAg8nfUCXm4d1wE66OyxNPTQc+s3/o/MoXizL3JE5O3F3P/uBZPPF4Zr+Wi5uSO48ZPRdyCwn7YB/A35m5KhWNHox4fcNnIs0ddOCRSBxf8+cQG+Huf0l8NJVYP+nI7NXy2ar4QqIGm69JfKPOE2w/mBavCzwM11R2D+ChsUO7hyUfmwx55qDM1xJvqZ7y08TpifuGBfjeURVJnNIVGpkNiXNS0ds7jcySDitDCCWW56LJ10fRo8sNA+3qXUSZD2CtQlZh9T+1rB7h9oliembflnMbzqgSNZKbKGHdPm7OwXb1CvQ1metSETMpszmzvikCJNh/h5E5PHNl4qga/+/cxqrdeWDYgIe7X5L4cGJPJX2940lOX8pD41FnFnc4riluvQKbK0dcHJFi2IBHNTQSlguru4d2/wPOTNzRA3x5y+U1E1uqWDkETOT026XuUJzx6u7ReLhSYenQ7uHua0fKZmwfmcPqsQjxE5WVONcRxn7X89zgn/EKPMRMxOVQXmP18Mx3q3b/Y/0cQE/IhFtHESMsHFlZ1Ml3CH3DZPHImY+pxcKumNmYirtvqMBfhMuU6s3iqOQkTsMPe1tCQwO8Ajs0lxr7W+vnp1MJc9EgCNd/cy6x+9D4veXmprj5wxMw/3C4egW6zzgZOlYZzfwo3F2J7ael0pJamvlPKgWNKFft1AAcKotXoFEbD7kaoSoQPVKB35+5KHF0lai/rJo+up87jWEE/qqqwY+qrL21LWLm95lPJ16ppKw31XC3PXYPJauPEx7B6BHCgrSizRs18qiaRp8tlN3ueCTYPHH9RNaunjI8Z7wLYpT3jZSCYXQ8e9vTsRE/q+no3XMKeObgGtaintbb/AvXj4JDkNw/5hrwYPfIvlZFUbLn7G5q+eQIN09Vnho6cqvnM/Lt99RixH49wO8K0ZL41WTWHoQzvsNVkOheZqKhEGpsp3SzB+BBtZAYve7uOR9tuTaaB6l0XScdYfEQPpkTUyHEGP+XqyDBzu+NBCITUjNWHynkrbWKOuWFn1xKzqsyx0bdvS78odp0+N503Zao0uCsWuSIDku8/7EO60b41vN5+Ses9BKlTdvd8bhp9EBvJjWJAIn/vxwHe6b3tSk6JFPV4nq85oAOrx555v/x/rh3E6Lo+bnuNS4uB4Cuq0ZfvO8X1rM6q/+vnjLVqZq7v83onttc2oYF4HPJmv1gWbB4P7s0l55ZsPhcsmY/WBYs3s8uzaVn5q3F/wf70mRuBCtbjQAAAABJRU5ErkJggg==', + }, + }).then((response: any) => { + if (response.status !== 200 && response.status !== 409) { + throw new Error(`Failed to create space test`); + } + }); +} diff --git a/x-pack/plugins/fleet/cypress/tsconfig.json b/x-pack/plugins/fleet/cypress/tsconfig.json index fe8ae5dc6806c..1422b6ff469cb 100644 --- a/x-pack/plugins/fleet/cypress/tsconfig.json +++ b/x-pack/plugins/fleet/cypress/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../../../tsconfig.base.json", "include": [ "**/*", - "../cypress.config.ts" + "../cypress.config.ts", + "../cypress.config.space_awareness.ts" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/fleet/package.json b/x-pack/plugins/fleet/package.json index 3e20162ab1d91..dc0bc6a6bcacb 100644 --- a/x-pack/plugins/fleet/package.json +++ b/x-pack/plugins/fleet/package.json @@ -5,6 +5,10 @@ "private": true, "license": "Elastic License 2.0", "scripts": { + "cypress_space_awareness": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../fleet/cypress.config.space_awareness.ts --ftr-config-file ../../../x-pack/test/fleet_cypress/cli_config.space_awareness", + "cypress_space_awareness:open": "yarn cypress_space_awareness open", + "cypress_space_awareness:run": "yarn cypress_space_awareness run", + "cypress_space_awareness:run:reporter": "yarn cypress_space_awareness run --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=../fleet/cypress/reporter_config.json", "cypress": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../fleet/cypress.config.ts --ftr-config-file ../../../x-pack/test/fleet_cypress/cli_config", "cypress:open": "yarn cypress open", "cypress:run": "yarn cypress run", diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 14dcf6df21b9f..6e4f1e06b45a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -244,6 +244,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( Object.keys(validation).length > 0 || hasAdvancedSettingsErrors } + data-test-subj="agentPolicyDetailsSaveButton" iconType="save" color="primary" fill diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx index 0643ac82634d9..7f4c1a3b91ead 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx @@ -90,6 +90,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.fleet.policyDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), + 'data-test-subj': 'agentPolicySettingsTab', href: getHref('policy_details', { policyId, tabId: 'settings' }), isSelected: tabId === 'settings', }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx index dcbd29c1ef74e..2682a5239071d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx @@ -356,6 +356,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { loading={isLoading} + data-test-subj="agentPoliciesTable" noItemsMessage={ isLoading ? ( { + const mockPackageInfo = { name: 'wiz' } as PackageInfo; + + beforeEach(() => { + (useLocalStorage as jest.Mock).mockClear(); + }); + + it('renders callout when package is wiz and callout is not dismissed', () => { + (useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]); + + render(); + + expect(screen.getByText(/New! Starting from version 1.9/)).toBeInTheDocument(); + }); + + it('does not render callout when package is not wiz', () => { + const nonWizPackageInfo = { name: 'other' } as PackageInfo; + render(); + + expect(screen.queryByText(/New! Starting from version 1.9/)).not.toBeInTheDocument(); + }); + + it('does not render callout when it has been dismissed', () => { + (useLocalStorage as jest.Mock).mockReturnValue([true, jest.fn()]); + + render(); + + expect(screen.queryByText(/New! Starting from version 1.9/)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/cloud_posture_third_party_support_callout.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/cloud_posture_third_party_support_callout.tsx new file mode 100644 index 0000000000000..6bd4197dc267e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/cloud_posture_third_party_support_callout.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import type { PackageInfo } from '../../../../../../../../common'; + +const LS_CLOUD_POSTURE_3P_SUPPORT_WIZ_INTEGRATIONS_CALLOUT_KEY = + 'fleet:cloudSecurityPosture:thirdPartySupport:wizIntegrationsCallout'; + +export const CloudPostureThirdPartySupportCallout = ({ + packageInfo, +}: { + packageInfo: PackageInfo; +}) => { + const [userHasDismissedWizCallout, setUserHasDismissedWizCallout] = useLocalStorage( + LS_CLOUD_POSTURE_3P_SUPPORT_WIZ_INTEGRATIONS_CALLOUT_KEY + ); + + if (packageInfo.name !== 'wiz' || userHasDismissedWizCallout) return null; + + return ( + <> + setUserHasDismissedWizCallout(true)} + iconType="cheer" + title={i18n.translate('xpack.fleet.epm.wizIntegration.newFeaturesCallout', { + defaultMessage: + 'New! Starting from version 1.9, ingest vulnerability and misconfiguration findings from Wiz into Elastic. Leverage out-of-the-box contextual investigation and threat-hunting workflows.', + })} + /> + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx index 9fa6df1e9f8ca..e96e74b1bb96c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx @@ -42,6 +42,8 @@ import { SideBarColumn } from '../../../components/side_bar_column'; import type { FleetStartServices } from '../../../../../../../plugin'; +import { CloudPostureThirdPartySupportCallout } from '../components/cloud_posture_third_party_support_callout'; + import { Screenshots } from './screenshots'; import { Readme } from './readme'; import { Details } from './details'; @@ -319,6 +321,7 @@ export const OverviewPage: React.FC = memo( )} + {isPrerelease && ( ". -2. If the samples have any or all of priority, timestamp, loglevel, hostname, ipAddress, messageId in the beginning information then set "header: true". -3. If the samples have a syslog header then set "header: true" , else set "header: false". If you are unable to determine the syslog header presence then set "header: false". -4. If the log samples have structured message body with key-value pairs then classify it as "name: structured". Look for a flat list of key-value pairs, often separated by spaces, commas, or other delimiters. -5. Consider variations in formatting, such as quotes around values ("key=value", key="value"), special characters in keys or values, or escape sequences. -6. If the log samples have unstructured body like a free-form text then classify it as "name: unstructured". -7. If the log samples follow a csv format then classify it with "name: csv". There are two sub-cases for csv: - a. If there is a csv header then set "header: true". - b. If there is no csv header then set "header: false" and try to find good names for the columns in the "columns" array by looking into the values of data in those columns. For each column, if you are unable to find good name candidate for it then output an empty string, like in the example. -8. If you cannot put the format into any of the above categories then classify it with "name: unsupported". +1. Go through each log sample and identify the log format. Output this as "name: ". Here are the values for log_format: + * 'csv': If the log samples follow a Comma-Separated Values format then classify it with "name: csv". There are two sub-cases for csv: + a. If there is a csv header then set "header: true". + b. If there is no csv header then set "header: false" and try to find good names for the columns in the "columns" array by looking into the values of data in those columns. For each column, if you are unable to find good name candidate for it then output an empty string, like in the example. + * 'structured': If the log samples have structured message body with key-value pairs then classify it as "name: structured". Look for a flat list of key-value pairs, often separated by some delimiters. Consider variations in formatting, such as quotes around values ("key=value", key="value"), special characters in keys or values, or escape sequences. + * 'unstructured': If the log samples have unstructured body like a free-form text then classify it as "name: unstructured". + * 'unsupported': If you cannot put the format into any of the above categories then classify it with "name: unsupported". +2. Header: for structured and unstructured format: + - if the samples have any or all of priority, timestamp, loglevel, hostname, ipAddress, messageId in the beginning information then set "header: true". + - if the samples have a syslog header then set "header: true" + - else set "header: false". If you are unable to determine the syslog header presence then set "header: false". +3. Note that a comma-separated list should be classified as 'csv' if its rows only contain values separated by commas. But if it looks like a list of comma separated key-values pairs like 'key1=value1, key2=value2' it should be classified as 'structured'. You ALWAYS follow these guidelines when writing your response: diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/generate.ts index a2f94fb437062..b13c3ff261207 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/generate.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/generate.ts @@ -79,7 +79,7 @@ export function generateFormula( } previousFormula += (previousColumn.filter.language === 'kuery' ? 'kql=' : 'lucene=') + - `'${previousColumn.filter.query.replace(/'/g, `\\'`)}'`; // replace all + `'${previousColumn.filter.query.replace(/\\/g, '\\\\').replace(/'/g, `\\'`)}'`; // replace all } if (previousColumn.timeShift) { if ( diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.test.tsx index 3d675883ec6bf..08a83ef7e0176 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { render, screen } from '@testing-library/react'; import { EuiButtonGroupProps, EuiButtonGroup } from '@elastic/eui'; import { DataDimensionEditor } from './dimension_editor'; import { FramePublicAPI, DatasourcePublicAPI } from '../../../types'; @@ -195,6 +196,65 @@ describe('XY Config panels', () => { expect(component.find(EuiColorPicker).prop('color')).toEqual('red'); }); + test.each<{ collapseFn?: string; shouldDisplay?: boolean }>([ + // should display color picker + { shouldDisplay: true }, + // should not display color picker + { collapseFn: 'sum', shouldDisplay: false }, + ])( + 'should only show color picker when collapseFn is defined for breakdown group', + ({ collapseFn = undefined, shouldDisplay = true }) => { + const state = { + ...testState(), + layers: [ + { + collapseFn, + seriesType: 'bar', + layerType: LayerTypes.DATA, + layerId: 'first', + splitAccessor: 'breakdownAccessor', + xAccessor: 'foo', + accessors: ['bar'], + yConfig: [{ forAccessor: 'bar', color: 'red' }], + }, + ], + } as XYState; + + render( + + ); + const colorPickerUi = screen.queryByLabelText('Edit colors'); + + if (shouldDisplay) { + expect(colorPickerUi).toBeInTheDocument(); + } else { + expect(colorPickerUi).not.toBeInTheDocument(); + } + } + ); test('does not apply incorrect color', () => { jest.useFakeTimers(); const setState = jest.fn(); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx index c0916b823466e..f06f2d861d865 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx @@ -134,8 +134,8 @@ export function DataDimensionEditor( const { splitAccessor } = layer; const splitCategories = getColorCategories(table?.rows ?? [], splitAccessor); - if (props.groupId === 'breakdown' && !layer.collapseFn) { - return ( + if (props.groupId === 'breakdown') { + return !layer.collapseFn ? ( - ); + ) : null; } const isHorizontal = isHorizontalChart(state.layers); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 76b2e17cb002e..dffe9628f29ec 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -695,6 +695,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource ); } + // @ts-expect-error hit's type is `SearchHit<{}>` while `flattenHit` expects `Record` const properties = indexPattern.flattenHit(hit) as Record; indexPattern.metaFields.forEach((metaField: string) => { if (!this._getTooltipPropertyNames().includes(metaField)) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx index 72e55b2564e35..d646b1e8b9ff5 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx @@ -82,7 +82,7 @@ function groupFieldsByOrigin(fields: StyleField[]) { type Props = { fields: StyleField[]; - selectedFieldName: string; + selectedFieldName?: string; onChange: ({ field }: { field: StyleField | null }) => void; styleName: VECTOR_STYLES; } & Omit< diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index c69e4d855ad90..e3751d9424dd0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -301,9 +301,7 @@ export class DynamicStyleProperty return this.getDataMappingFunction() === DATA_MAPPING_FUNCTION.INTERPOLATE ? this._field.getExtendedStatsFieldMetaRequest() : this._field.getPercentilesFieldMetaRequest( - this.getFieldMetaOptions().percentiles !== undefined - ? this.getFieldMetaOptions().percentiles - : DEFAULT_PERCENTILES + this.getFieldMetaOptions().percentiles ?? DEFAULT_PERCENTILES ); } @@ -331,7 +329,7 @@ export class DynamicStyleProperty return this.usesFeatureState() ? MB_LOOKUP_FUNCTION.FEATURE_STATE : MB_LOOKUP_FUNCTION.GET; } - getFieldMetaOptions() { + getFieldMetaOptions(): FieldMetaOptions { const fieldMetaOptions = _.get(this.getOptions(), 'fieldMetaOptions', { isEnabled: true }); // In 8.0, UI changed to not allow setting isEnabled to false when fieldMeta from local not supported diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx index 1f54801118f30..96b1c99e98bbc 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx @@ -69,7 +69,7 @@ export class Join extends Component { this._isMounted = false; } - async _loadRightFields(indexPatternId: string) { + async _loadRightFields(indexPatternId?: string) { if (!indexPatternId) { return; } diff --git a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts index 771e40fc13336..2220cf86d3edb 100644 --- a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts +++ b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts @@ -1237,11 +1237,11 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu // If the job uses aggregation or scripted fields, and if it's a config we don't support // use model plot data if model plot is enabled // else if source data can be plotted, use that, otherwise model plot will be available. - // @ts-ignore + // @ts-expect-error const useSourceData = isSourceDataChartableForDetector(job, detectorIndex); if (useSourceData) { - const datafeedQuery = get(config, 'datafeedConfig.query', null); + const datafeedQuery = get(config, 'datafeedConfig.query'); try { return await fetchMetricData( diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_title.ts b/x-pack/plugins/monitoring/public/application/hooks/use_title.ts index a4661e71eb399..ace9bd92b0a2e 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_title.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_title.ts @@ -11,7 +11,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; // TODO: verify that works for all pages export function useTitle(cluster: string, suffix: string) { const { services } = useKibana(); - let clusterName = get(cluster, 'cluster_name'); + let clusterName: string | undefined = get(cluster, 'cluster_name'); clusterName = clusterName ? `- ${clusterName}` : ''; suffix = suffix ? `- ${suffix}` : ''; diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 1c9f0b3ee9acc..073da0ca73bb4 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -92,7 +92,7 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { - const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); + const liveClusterUuid: string | undefined = get(data, '_meta.liveClusterUuid'); const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( (node: any) => node.isPartiallyMigrated || node.isFullyMigrated ); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts index ba099f1e7e7a8..ede6d1edf0848 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts @@ -41,6 +41,6 @@ describe('fetchLicenseType', () => { })), } as unknown as ElasticsearchClient; const result = await fetchLicenseType(customCallCluster, availableCcs, clusterUuid); - expect(result).toStrictEqual(null); + expect(result).toBeUndefined(); }); }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts index 6601c350d4922..86761262c886f 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts @@ -56,5 +56,5 @@ export async function fetchLicenseType( }, }; const response = await client.search(params); - return get(response, 'hits.hits[0]._source.license.type', null); + return get(response, 'hits.hits[0]._source.license.type') as string | undefined; } diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts index 2f2437b325b5a..8914d20942551 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts @@ -12,7 +12,7 @@ export interface MonitoringUsage { export interface MonitoringClusterStackProductUsage { clusterUuid: string; - license: string; + license?: string; metricbeatUsed: boolean; elasticsearch: StackProductUsage; logstash: StackProductUsage; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 77c96e8b6138a..ddd9b79d09cb4 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -82,7 +82,7 @@ describe('fetchCpuUsageNodeStats', () => { containerUsage: undefined, containerPeriods: undefined, containerQuota: undefined, - ccs: null, + ccs: undefined, }, ]); }); @@ -149,7 +149,7 @@ describe('fetchCpuUsageNodeStats', () => { containerUsage: 10, containerPeriods: 5, containerQuota: 50, - ccs: null, + ccs: undefined, }, ]); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 8037ad94e6764..b74b1b78b495b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -170,12 +170,12 @@ export async function fetchCpuUsageNodeStats( const stat = { clusterUuid: clusterBucket.key, nodeId: node.key, - nodeName: get(node, 'name.buckets[0].key'), - cpuUsage: get(node, 'average_cpu.value'), - containerUsage: get(lastBucket, 'usage_deriv.normalized_value'), - containerPeriods: get(lastBucket, 'periods_deriv.normalized_value'), - containerQuota: get(node, 'average_quota.value'), - ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + nodeName: get(node, 'name.buckets[0].key') as unknown as string, + cpuUsage: get(node, 'average_cpu.value') as number, + containerUsage: get(lastBucket, 'usage_deriv.normalized_value') as unknown as number, + containerPeriods: get(lastBucket, 'periods_deriv.normalized_value') as unknown as number, + containerQuota: get(node, 'average_quota.value') as unknown as number, + ccs: indexName.includes(':') ? indexName.split(':')[0] : undefined, }; stats.push(stat); } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts index 0e6fea87904e5..7a70a8594936f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts @@ -105,14 +105,14 @@ describe('fetchMissingMonitoringData', () => { nodeName: 'nodeName1', clusterUuid: 'clusterUuid1', gapDuration: 1, - ccs: null, + ccs: undefined, }, { nodeId: 'nodeUuid2', nodeName: 'nodeName2', clusterUuid: 'clusterUuid1', gapDuration: 8, - ccs: null, + ccs: undefined, }, ]); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index f49d068864c69..35f37c14b98ac 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -163,7 +163,7 @@ export async function fetchMissingMonitoringData( nodeName, clusterUuid, gapDuration: differenceInMs, - ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + ccs: indexName.includes(':') ? indexName.split(':')[0] : undefined, }; } } diff --git a/x-pack/plugins/monitoring/server/lib/details/get_series.ts b/x-pack/plugins/monitoring/server/lib/details/get_series.ts index 3c1844a8cf0ab..3b325864d8036 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_series.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_series.ts @@ -64,7 +64,7 @@ function defaultCalculation( metric?: Metric, defaultSizeInSeconds?: number ) { - const legacyValue: number = get(bucket, key, null); + const legacyValue: number = get(bucket, key, -1); const mbValue = bucket.metric_mb_deriv?.normalized_value ?? null; let value; if (mbValue !== null && !isNaN(mbValue) && mbValue > 0) { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts index 17a8909752103..d5af43e216834 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stats.ts @@ -37,7 +37,7 @@ export function handleResponse( cluster, 'elasticsearch.cluster.stats.state.master_node', get(cluster, 'cluster_state.master_node') - ); + ) as string; nodes = resp.aggregations?.nodes.buckets.reduce(normalizeNodeShards(masterNode), {}) ?? []; } diff --git a/x-pack/plugins/monitoring/server/lib/metrics/classes/quota_metric.ts b/x-pack/plugins/monitoring/server/lib/metrics/classes/quota_metric.ts index 8e1cb8930f804..e83818052f54b 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/classes/quota_metric.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/classes/quota_metric.ts @@ -66,7 +66,7 @@ export class QuotaMetric extends Metric { }; this.calculation = (bucket: object) => { - const quota = get(bucket, 'quota.value'); + const quota = get(bucket, 'quota.value', 0); const deltaUsageDerivNormalizedValue = get(bucket, 'usage_deriv.normalized_value'); const periodsDerivNormalizedValue = get(bucket, 'periods_deriv.normalized_value'); diff --git a/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/classes.ts b/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/classes.ts index c6c49222e20fa..7cc9ea644b599 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/classes.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/classes.ts @@ -82,7 +82,7 @@ export class DifferenceMetric extends ElasticsearchMetric { this.getFields = () => [`${fieldSource}.${metric}`, `${fieldSource}.${metric2}`]; this.calculation = (bucket: object) => { - return _.get(bucket, 'metric_max.value') - _.get(bucket, 'metric2_max.value'); + return _.get(bucket, 'metric_max.value', 0) - _.get(bucket, 'metric2_max.value', 0); }; } } @@ -369,7 +369,8 @@ export class WriteThreadPoolRejectedMetric extends ElasticsearchMetric { const write = _.get(bucket, 'write_deriv.normalized_value', null); if (index !== null || bulk !== null || write !== null) { - const valueOrZero = (value: number) => (value < 0 ? 0 : value || 0); + const valueOrZero = (value: number | null) => + value === null || value < 0 ? 0 : value || 0; return valueOrZero(index) + valueOrZero(bulk) + valueOrZero(write); } @@ -394,7 +395,7 @@ export class MillisecondsToSecondsMetric extends ElasticsearchMetric { }), }); this.calculation = (bucket: object) => { - return _.get(bucket, 'metric.value') / 1000; + return _.get(bucket, 'metric.value', 0) / 1000; }; } } diff --git a/x-pack/plugins/monitoring/server/lib/metrics/logstash/classes.ts b/x-pack/plugins/monitoring/server/lib/metrics/logstash/classes.ts index 790f761637dbd..fd150864c6b2e 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/logstash/classes.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/logstash/classes.ts @@ -301,8 +301,8 @@ export class LogstashPipelineQueueSizeMetric extends LogstashMetric { }, }; this.calculation = (bucket: object) => { - const legacyQueueSize = _.get(bucket, 'pipelines.total_queue_size_for_node.value'); - const mbQueueSize = _.get(bucket, 'pipelines_mb.total_queue_size_for_node.value'); + const legacyQueueSize = _.get(bucket, 'pipelines.total_queue_size_for_node.value', 0); + const mbQueueSize = _.get(bucket, 'pipelines_mb.total_queue_size_for_node.value', 0); return Math.max(legacyQueueSize, mbQueueSize); }; } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts index 137175010cb22..7c939407ef156 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts @@ -328,6 +328,7 @@ export function ccrRoute(server: MonitoringCore) { ); const follows = remoteCluster ? `${leaderIndex} on ${remoteCluster}` : leaderIndex; + // @ts-expect-error `shards.error` type mismatch (error: string | undefined vs. error: { error: string | undefined }) const shards: CcrShard[] = get(bucket, 'by_shard_id.buckets').map( (shardBucket: CcrShardBucket) => buildShardStats({ bucket, fullStats, shardBucket }) ); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.ts index d420056d4ec0e..9aed104920e83 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.ts @@ -91,7 +91,7 @@ export function esIndexRoute(server: MonitoringCore) { cluster, 'elasticsearch.cluster.stats.state.state_uuid', get(cluster, 'cluster_state.state_uuid') - ); + ) as string; const allocationOptions = { shardFilter, stateUuid, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.ts index a07b55b9be635..763f8005abc93 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.ts @@ -94,6 +94,7 @@ export function esNodeRoute(server: MonitoringCore) { includeNodes: true, nodeUuid, }); + // @ts-expect-error `clusterState.master_node` types are incompatible const nodeSummary = await getNodeSummary(req, clusterState, shardStats, { clusterUuid, nodeUuid, @@ -119,7 +120,7 @@ export function esNodeRoute(server: MonitoringCore) { cluster, 'cluster_state.state_uuid', get(cluster, 'elasticsearch.cluster.stats.state.state_uuid') - ); + )!; const allocationOptions = { shardFilter, stateUuid, diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts index acae6ff11e12c..e8b53a1ed1adc 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts @@ -156,12 +156,14 @@ function groupInstancesByCluster( // hits are sorted arbitrarily by product UUID instances.map((instance) => { const clusterUuid = instance._source!.cluster_uuid; - const version: string | undefined = get( - instance, - `_source.${product}_stats.${product}.version` - ); - const cloud: CloudEntry | undefined = get(instance, `_source.${product}_stats.cloud`); - const os: OSData | undefined = get(instance, `_source.${product}_stats.os`); + const version: string | undefined = get(instance, [ + `_source`, + `${product}_stats`, + product, + `version`, + ]); + const cloud: CloudEntry | undefined = get(instance, [`_source`, `${product}_stats`, `cloud`]); + const os: OSData | undefined = get(instance, [`_source`, `${product}_stats`, `os`]); if (clusterUuid) { let cluster = clusterMap.get(clusterUuid); diff --git a/x-pack/plugins/observability_solution/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap b/x-pack/plugins/observability_solution/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap index 6fa3e146a423d..88d00196e074b 100644 --- a/x-pack/plugins/observability_solution/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap +++ b/x-pack/plugins/observability_solution/apm/common/es_fields/__snapshots__/es_fields.test.ts.snap @@ -37,6 +37,8 @@ Object { exports[`Error CLOUD_ACCOUNT_ID 1`] = `undefined`; +exports[`Error CLOUD_ACCOUNT_NAME 1`] = `undefined`; + exports[`Error CLOUD_AVAILABILITY_ZONE 1`] = `"europe-west1-c"`; exports[`Error CLOUD_INSTANCE_ID 1`] = `undefined`; @@ -45,6 +47,8 @@ exports[`Error CLOUD_INSTANCE_NAME 1`] = `undefined`; exports[`Error CLOUD_MACHINE_TYPE 1`] = `undefined`; +exports[`Error CLOUD_PROJECT_NAME 1`] = `undefined`; + exports[`Error CLOUD_PROVIDER 1`] = `"gcp"`; exports[`Error CLOUD_REGION 1`] = `"europe-west1"`; @@ -94,6 +98,8 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Error ERROR_PAGE_URL 1`] = `undefined`; +exports[`Error ERROR_STACK_TRACE 1`] = `undefined`; + exports[`Error ERROR_TYPE 1`] = `undefined`; exports[`Error EVENT_NAME 1`] = `undefined`; @@ -140,6 +146,8 @@ exports[`Error INDEX 1`] = `undefined`; exports[`Error KUBERNETES 1`] = `undefined`; +exports[`Error KUBERNETES_CONTAINER_ID 1`] = `undefined`; + exports[`Error KUBERNETES_CONTAINER_NAME 1`] = `undefined`; exports[`Error KUBERNETES_DEPLOYMENT 1`] = `undefined`; @@ -150,6 +158,8 @@ exports[`Error KUBERNETES_NAMESPACE 1`] = `undefined`; exports[`Error KUBERNETES_NAMESPACE_NAME 1`] = `undefined`; +exports[`Error KUBERNETES_NODE_NAME 1`] = `undefined`; + exports[`Error KUBERNETES_POD_NAME 1`] = `undefined`; exports[`Error KUBERNETES_POD_UID 1`] = `undefined`; @@ -228,10 +238,20 @@ exports[`Error OBSERVER_HOSTNAME 1`] = `undefined`; exports[`Error OBSERVER_LISTENING 1`] = `undefined`; +exports[`Error OBSERVER_VERSION 1`] = `"whatever"`; + +exports[`Error OBSERVER_VERSION_MAJOR 1`] = `8`; + exports[`Error PARENT_ID 1`] = `"parentId"`; +exports[`Error PROCESS_ARGS 1`] = `undefined`; + +exports[`Error PROCESS_PID 1`] = `undefined`; + exports[`Error PROCESSOR_EVENT 1`] = `"error"`; +exports[`Error PROCESSOR_NAME 1`] = `"error"`; + exports[`Error SERVICE 1`] = ` Object { "language": Object { @@ -296,6 +316,8 @@ exports[`Error SPAN_NAME 1`] = `undefined`; exports[`Error SPAN_SELF_TIME_SUM 1`] = `undefined`; +exports[`Error SPAN_STACKTRACE 1`] = `undefined`; + exports[`Error SPAN_SUBTYPE 1`] = `undefined`; exports[`Error SPAN_SYNC 1`] = `undefined`; @@ -304,10 +326,12 @@ exports[`Error SPAN_TYPE 1`] = `undefined`; exports[`Error TIER 1`] = `undefined`; -exports[`Error TIMESTAMP 1`] = `1337`; +exports[`Error TIMESTAMP_US 1`] = `1337`; exports[`Error TRACE_ID 1`] = `"trace id"`; +exports[`Error TRANSACTION_AGENT_MARKS 1`] = `undefined`; + exports[`Error TRANSACTION_DURATION 1`] = `undefined`; exports[`Error TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`; @@ -385,6 +409,8 @@ Object { exports[`Span CLOUD_ACCOUNT_ID 1`] = `undefined`; +exports[`Span CLOUD_ACCOUNT_NAME 1`] = `undefined`; + exports[`Span CLOUD_AVAILABILITY_ZONE 1`] = `"europe-west1-c"`; exports[`Span CLOUD_INSTANCE_ID 1`] = `undefined`; @@ -393,6 +419,8 @@ exports[`Span CLOUD_INSTANCE_NAME 1`] = `undefined`; exports[`Span CLOUD_MACHINE_TYPE 1`] = `undefined`; +exports[`Span CLOUD_PROJECT_NAME 1`] = `undefined`; + exports[`Span CLOUD_PROVIDER 1`] = `"gcp"`; exports[`Span CLOUD_REGION 1`] = `"europe-west1"`; @@ -433,6 +461,8 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Span ERROR_PAGE_URL 1`] = `undefined`; +exports[`Span ERROR_STACK_TRACE 1`] = `undefined`; + exports[`Span ERROR_TYPE 1`] = `undefined`; exports[`Span EVENT_NAME 1`] = `undefined`; @@ -475,6 +505,8 @@ exports[`Span INDEX 1`] = `undefined`; exports[`Span KUBERNETES 1`] = `undefined`; +exports[`Span KUBERNETES_CONTAINER_ID 1`] = `undefined`; + exports[`Span KUBERNETES_CONTAINER_NAME 1`] = `undefined`; exports[`Span KUBERNETES_DEPLOYMENT 1`] = `undefined`; @@ -485,6 +517,8 @@ exports[`Span KUBERNETES_NAMESPACE 1`] = `undefined`; exports[`Span KUBERNETES_NAMESPACE_NAME 1`] = `undefined`; +exports[`Span KUBERNETES_NODE_NAME 1`] = `undefined`; + exports[`Span KUBERNETES_POD_NAME 1`] = `undefined`; exports[`Span KUBERNETES_POD_UID 1`] = `undefined`; @@ -563,10 +597,20 @@ exports[`Span OBSERVER_HOSTNAME 1`] = `undefined`; exports[`Span OBSERVER_LISTENING 1`] = `undefined`; +exports[`Span OBSERVER_VERSION 1`] = `"whatever"`; + +exports[`Span OBSERVER_VERSION_MAJOR 1`] = `8`; + exports[`Span PARENT_ID 1`] = `"parentId"`; +exports[`Span PROCESS_ARGS 1`] = `undefined`; + +exports[`Span PROCESS_PID 1`] = `undefined`; + exports[`Span PROCESSOR_EVENT 1`] = `"span"`; +exports[`Span PROCESSOR_NAME 1`] = `"transaction"`; + exports[`Span SERVICE 1`] = ` Object { "name": "service name", @@ -627,6 +671,8 @@ exports[`Span SPAN_NAME 1`] = `"span name"`; exports[`Span SPAN_SELF_TIME_SUM 1`] = `undefined`; +exports[`Span SPAN_STACKTRACE 1`] = `undefined`; + exports[`Span SPAN_SUBTYPE 1`] = `"my subtype"`; exports[`Span SPAN_SYNC 1`] = `false`; @@ -635,10 +681,12 @@ exports[`Span SPAN_TYPE 1`] = `"span type"`; exports[`Span TIER 1`] = `undefined`; -exports[`Span TIMESTAMP 1`] = `1337`; +exports[`Span TIMESTAMP_US 1`] = `1337`; exports[`Span TRACE_ID 1`] = `"trace id"`; +exports[`Span TRANSACTION_AGENT_MARKS 1`] = `undefined`; + exports[`Span TRANSACTION_DURATION 1`] = `undefined`; exports[`Span TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`; @@ -716,6 +764,8 @@ Object { exports[`Transaction CLOUD_ACCOUNT_ID 1`] = `undefined`; +exports[`Transaction CLOUD_ACCOUNT_NAME 1`] = `undefined`; + exports[`Transaction CLOUD_AVAILABILITY_ZONE 1`] = `"europe-west1-c"`; exports[`Transaction CLOUD_INSTANCE_ID 1`] = `undefined`; @@ -724,6 +774,8 @@ exports[`Transaction CLOUD_INSTANCE_NAME 1`] = `undefined`; exports[`Transaction CLOUD_MACHINE_TYPE 1`] = `undefined`; +exports[`Transaction CLOUD_PROJECT_NAME 1`] = `undefined`; + exports[`Transaction CLOUD_PROVIDER 1`] = `"gcp"`; exports[`Transaction CLOUD_REGION 1`] = `"europe-west1"`; @@ -768,6 +820,8 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`; +exports[`Transaction ERROR_STACK_TRACE 1`] = `undefined`; + exports[`Transaction ERROR_TYPE 1`] = `undefined`; exports[`Transaction EVENT_NAME 1`] = `undefined`; @@ -820,6 +874,8 @@ Object { } `; +exports[`Transaction KUBERNETES_CONTAINER_ID 1`] = `undefined`; + exports[`Transaction KUBERNETES_CONTAINER_NAME 1`] = `undefined`; exports[`Transaction KUBERNETES_DEPLOYMENT 1`] = `undefined`; @@ -830,6 +886,8 @@ exports[`Transaction KUBERNETES_NAMESPACE 1`] = `undefined`; exports[`Transaction KUBERNETES_NAMESPACE_NAME 1`] = `undefined`; +exports[`Transaction KUBERNETES_NODE_NAME 1`] = `undefined`; + exports[`Transaction KUBERNETES_POD_NAME 1`] = `undefined`; exports[`Transaction KUBERNETES_POD_UID 1`] = `"pod1234567890abcdef"`; @@ -908,10 +966,20 @@ exports[`Transaction OBSERVER_HOSTNAME 1`] = `undefined`; exports[`Transaction OBSERVER_LISTENING 1`] = `undefined`; +exports[`Transaction OBSERVER_VERSION 1`] = `"whatever"`; + +exports[`Transaction OBSERVER_VERSION_MAJOR 1`] = `8`; + exports[`Transaction PARENT_ID 1`] = `"parentId"`; +exports[`Transaction PROCESS_ARGS 1`] = `undefined`; + +exports[`Transaction PROCESS_PID 1`] = `undefined`; + exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`; +exports[`Transaction PROCESSOR_NAME 1`] = `"transaction"`; + exports[`Transaction SERVICE 1`] = ` Object { "language": Object { @@ -976,6 +1044,8 @@ exports[`Transaction SPAN_NAME 1`] = `undefined`; exports[`Transaction SPAN_SELF_TIME_SUM 1`] = `undefined`; +exports[`Transaction SPAN_STACKTRACE 1`] = `undefined`; + exports[`Transaction SPAN_SUBTYPE 1`] = `undefined`; exports[`Transaction SPAN_SYNC 1`] = `undefined`; @@ -984,10 +1054,12 @@ exports[`Transaction SPAN_TYPE 1`] = `undefined`; exports[`Transaction TIER 1`] = `undefined`; -exports[`Transaction TIMESTAMP 1`] = `1337`; +exports[`Transaction TIMESTAMP_US 1`] = `1337`; exports[`Transaction TRACE_ID 1`] = `"trace id"`; +exports[`Transaction TRANSACTION_AGENT_MARKS 1`] = `undefined`; + exports[`Transaction TRANSACTION_DURATION 1`] = `1337`; exports[`Transaction TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`; diff --git a/x-pack/plugins/observability_solution/apm/common/es_fields/es_fields.test.ts b/x-pack/plugins/observability_solution/apm/common/es_fields/es_fields.test.ts index f33fddd430e8d..12537d35afefe 100644 --- a/x-pack/plugins/observability_solution/apm/common/es_fields/es_fields.test.ts +++ b/x-pack/plugins/observability_solution/apm/common/es_fields/es_fields.test.ts @@ -10,12 +10,13 @@ import { AllowUnknownProperties } from '../../typings/common'; import { APMError } from '../../typings/es_schemas/ui/apm_error'; import { Span } from '../../typings/es_schemas/ui/span'; import { Transaction } from '../../typings/es_schemas/ui/transaction'; -import * as apmFieldnames from './apm'; -import * as infraMetricsFieldnames from './infra_metrics'; +import * as allApmFieldNames from './apm'; +import * as infraMetricsFieldNames from './infra_metrics'; +const { AT_TIMESTAMP, ...apmFieldNames } = allApmFieldNames; const fieldnames = { - ...apmFieldnames, - ...infraMetricsFieldnames, + ...apmFieldNames, + ...infraMetricsFieldNames, }; describe('Transaction', () => { diff --git a/x-pack/plugins/observability_solution/apm/common/service_metadata.ts b/x-pack/plugins/observability_solution/apm/common/service_metadata.ts index 0ccede67741b7..4136ea361392e 100644 --- a/x-pack/plugins/observability_solution/apm/common/service_metadata.ts +++ b/x-pack/plugins/observability_solution/apm/common/service_metadata.ts @@ -4,5 +4,63 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { + CLOUD_AVAILABILITY_ZONE, + CLOUD_INSTANCE_ID, + CLOUD_INSTANCE_NAME, + CLOUD_MACHINE_TYPE, + CLOUD_PROVIDER, + CONTAINER_ID, + HOST_NAME, + KUBERNETES_CONTAINER_NAME, + KUBERNETES_NAMESPACE, + KUBERNETES_DEPLOYMENT_NAME, + KUBERNETES_POD_NAME, + KUBERNETES_POD_UID, + KUBERNETES_REPLICASET_NAME, + SERVICE_NODE_NAME, + SERVICE_RUNTIME_NAME, + SERVICE_RUNTIME_VERSION, + SERVICE_VERSION, +} from './es_fields/apm'; +import { asMutableArray } from './utils/as_mutable_array'; + +export const SERVICE_METADATA_SERVICE_KEYS = asMutableArray([ + SERVICE_NODE_NAME, + SERVICE_VERSION, + SERVICE_RUNTIME_NAME, + SERVICE_RUNTIME_VERSION, +] as const); + +export const SERVICE_METADATA_CONTAINER_KEYS = asMutableArray([ + CONTAINER_ID, + HOST_NAME, + KUBERNETES_POD_UID, + KUBERNETES_POD_NAME, +] as const); + +export const SERVICE_METADATA_INFRA_METRICS_KEYS = asMutableArray([ + KUBERNETES_CONTAINER_NAME, + KUBERNETES_NAMESPACE, + KUBERNETES_REPLICASET_NAME, + KUBERNETES_DEPLOYMENT_NAME, +] as const); + +export const SERVICE_METADATA_CLOUD_KEYS = asMutableArray([ + CLOUD_AVAILABILITY_ZONE, + CLOUD_INSTANCE_ID, + CLOUD_INSTANCE_NAME, + CLOUD_MACHINE_TYPE, + CLOUD_PROVIDER, +] as const); + +export const SERVICE_METADATA_KUBERNETES_KEYS = asMutableArray([ + KUBERNETES_CONTAINER_NAME, + KUBERNETES_NAMESPACE, + KUBERNETES_DEPLOYMENT_NAME, + KUBERNETES_POD_NAME, + KUBERNETES_POD_UID, + KUBERNETES_REPLICASET_NAME, +] as const); export type ContainerType = 'Kubernetes' | 'Docker' | undefined; diff --git a/x-pack/plugins/observability_solution/apm/common/waterfall/typings.ts b/x-pack/plugins/observability_solution/apm/common/waterfall/typings.ts index 49f282473e9dd..2fd0be94a5c5f 100644 --- a/x-pack/plugins/observability_solution/apm/common/waterfall/typings.ts +++ b/x-pack/plugins/observability_solution/apm/common/waterfall/typings.ts @@ -64,16 +64,17 @@ export interface WaterfallSpan { links?: SpanLink[]; }; transaction?: { - id: string; + id?: string; }; child?: { id: string[] }; } export interface WaterfallError { timestamp: TimestampUs; - trace?: { id: string }; - transaction?: { id: string }; - parent?: { id: string }; + trace?: { id?: string }; + transaction?: { id?: string }; + parent?: { id?: string }; + span?: { id?: string }; error: { id: string; log?: { diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_contextual_insight.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_contextual_insight.tsx index 2e91865083b8c..20d5521b43ebf 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_contextual_insight.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_contextual_insight.tsx @@ -8,9 +8,9 @@ import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { Message } from '@kbn/observability-ai-assistant-plugin/public'; import React, { useMemo, useState } from 'react'; +import { AT_TIMESTAMP } from '@kbn/apm-types'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { ErrorSampleDetailTabContent } from './error_sample_detail'; import { exceptionStacktraceTab, logStacktraceTab } from './error_tabs'; @@ -18,8 +18,26 @@ export function ErrorSampleContextualInsight({ error, transaction, }: { - error: APMError; - transaction?: Transaction; + error: { + [AT_TIMESTAMP]: string; + error: Pick; + service: { + name: string; + environment?: string; + language?: { + name?: string; + }; + runtime?: { + name?: string; + version?: string; + }; + }; + }; + transaction?: { + transaction: { + name: string; + }; + }; }) { const { observabilityAIAssistant } = useApmPluginContext(); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx index a38c4dfc96f63..2edb2c1a3fea6 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_sample_detail.tsx @@ -29,14 +29,14 @@ import { first } from 'lodash'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; -import { ERROR_GROUP_ID } from '../../../../../common/es_fields/apm'; +import { AT_TIMESTAMP, ERROR_GROUP_ID } from '../../../../../common/es_fields/apm'; import { TraceSearchType } from '../../../../../common/trace_explorer'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; -import { FETCH_STATUS, isPending } from '../../../../hooks/use_fetcher'; +import { FETCH_STATUS, isPending, isSuccess } from '../../../../hooks/use_fetcher'; import { useTraceExplorerEnabledSetting } from '../../../../hooks/use_trace_explorer_enabled_setting'; import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link'; @@ -111,8 +111,7 @@ export function ErrorSampleDetails({ const loadingErrorData = isPending(errorFetchStatus); const isLoading = loadingErrorSamplesData || loadingErrorData; - const isSucceded = - errorSamplesFetchStatus === FETCH_STATUS.SUCCESS && errorFetchStatus === FETCH_STATUS.SUCCESS; + const isSucceeded = isSuccess(errorSamplesFetchStatus) && isSuccess(errorFetchStatus); useEffect(() => { setSampleActivePage(0); @@ -137,7 +136,7 @@ export function ErrorSampleDetails({ }); }, [error, transaction, uiActions]); - if (!error && errorSampleIds?.length === 0 && isSucceded) { + if (!error && errorSampleIds?.length === 0 && isSucceeded) { return ( ; + }; currentTab: ErrorTab; }) { const codeLanguage = error?.service.language?.name; const exceptions = error?.error.exception || []; const logStackframes = error?.error.log?.stacktrace; const isPlaintextException = - !!error?.error.stack_trace && exceptions.length === 1 && !exceptions[0].stacktrace; + !!error.error.stack_trace && exceptions.length === 1 && !exceptions[0].stacktrace; switch (currentTab.key) { case ErrorTabKey.LogStackTrace: return ; @@ -363,7 +370,7 @@ export function ErrorSampleDetailTabContent({ return isPlaintextException ? ( diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_tabs.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_tabs.tsx index 893e842513c8f..86b69eb480b3f 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_tabs.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/error_tabs.tsx @@ -41,7 +41,7 @@ export const metadataTab: ErrorTab = { }), }; -export function getTabs(error: APMError) { +export function getTabs(error: { error: { log?: APMError['error']['log'] } }) { const hasLogStacktrace = !isEmpty(error?.error.log?.stacktrace); return [...(hasLogStacktrace ? [logStacktraceTab] : []), exceptionStacktraceTab, metadataTab]; } diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/sample_summary.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/sample_summary.tsx index af05e8766994c..c7acbfee7e45e 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/sample_summary.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/error_group_details/error_sampler/sample_summary.tsx @@ -18,7 +18,9 @@ const Label = euiStyled.div` `; interface Props { - error: APMError; + error: { + error: Pick; + }; } export function SampleSummary({ error }: Props) { const logMessage = error.error.log?.message; diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts b/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts index a3467d7272ff5..acd9e9445ad8f 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts @@ -6,7 +6,7 @@ */ import { format } from 'url'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import type { TransactionDetailRedirectInfo } from '../../../../server/routes/transactions/get_transaction_by_trace'; export const getRedirectToTransactionDetailPageUrl = ({ transaction, @@ -14,7 +14,7 @@ export const getRedirectToTransactionDetailPageUrl = ({ rangeTo, waterfallItemId, }: { - transaction: Transaction; + transaction: TransactionDetailRedirectInfo; rangeFrom?: string; rangeTo?: string; waterfallItemId?: string; diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/links/discover_links/discover_error_link.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/links/discover_links/discover_error_link.tsx index 2958d2af7d68f..a32c01f3b15e5 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/links/discover_links/discover_error_link.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/links/discover_links/discover_error_link.tsx @@ -7,10 +7,18 @@ import React, { ReactNode } from 'react'; import { ERROR_GROUP_ID, SERVICE_NAME } from '../../../../../common/es_fields/apm'; -import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { DiscoverLink } from './discover_link'; -function getDiscoverQuery(error: APMError, kuery?: string) { +interface ErrorForDiscoverQuery { + service: { + name: string; + }; + error: { + grouping_key: string; + }; +} + +function getDiscoverQuery(error: ErrorForDiscoverQuery, kuery?: string) { const serviceName = error.service.name; const groupId = error.error.grouping_key; let query = `${SERVICE_NAME}:"${serviceName}" AND ${ERROR_GROUP_ID}:"${groupId}"`; @@ -36,7 +44,7 @@ function DiscoverErrorLink({ children, }: { children?: ReactNode; - readonly error: APMError; + readonly error: ErrorForDiscoverQuery; readonly kuery?: string; }) { return ; diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/metadata_table/error_metadata/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/metadata_table/error_metadata/index.tsx index ae688f8917602..dab585180fce9 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/metadata_table/error_metadata/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/metadata_table/error_metadata/index.tsx @@ -7,13 +7,16 @@ import React, { useMemo } from 'react'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { APMError, AT_TIMESTAMP } from '@kbn/apm-types'; import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; interface Props { - error: APMError; + error: { + [AT_TIMESTAMP]: string; + error: Pick; + }; } export function ErrorMetadata({ error }: Props) { @@ -26,8 +29,8 @@ export function ErrorMetadata({ error }: Props) { id: error.error.id, }, query: { - start: error['@timestamp'], - end: error['@timestamp'], + start: error[AT_TIMESTAMP], + end: error[AT_TIMESTAMP], }, }, }); diff --git a/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts index 319582f61b664..a2b6809f855e7 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts @@ -8,7 +8,7 @@ import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; import { tasks } from './tasks'; -import { SERVICE_NAME, SERVICE_ENVIRONMENT } from '../../../../common/es_fields/apm'; +import { SERVICE_NAME, SERVICE_ENVIRONMENT, AT_TIMESTAMP } from '../../../../common/es_fields/apm'; import { IndicesStatsResponse } from '../telemetry_client'; describe('data telemetry collection tasks', () => { @@ -101,7 +101,7 @@ describe('data telemetry collection tasks', () => { // a fixed date range. .mockReturnValueOnce({ hits: { - hits: [{ _source: { '@timestamp': new Date().toISOString() } }], + hits: [{ fields: { [AT_TIMESTAMP]: [new Date().toISOString()] } }], }, total: { value: 1, @@ -314,7 +314,7 @@ describe('data telemetry collection tasks', () => { ? { hits: { total: { value: 1 } } } : { hits: { - hits: [{ _source: { '@timestamp': 1 } }], + hits: [{ fields: { [AT_TIMESTAMP]: [1] } }], }, } ); diff --git a/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index db6a6a918177a..1347cbb4e3641 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -11,11 +11,13 @@ import { createHash } from 'crypto'; import { flatten, merge, pickBy, sortBy, sum, uniq } from 'lodash'; import { SavedObjectsClient } from '@kbn/core/server'; import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; import { AGENT_NAMES, RUM_AGENT_NAMES } from '../../../../common/agent_name'; import { AGENT_ACTIVATION_METHOD, AGENT_NAME, AGENT_VERSION, + AT_TIMESTAMP, CLIENT_GEO_COUNTRY_ISO_CODE, CLOUD_AVAILABILITY_ZONE, CLOUD_PROVIDER, @@ -29,6 +31,7 @@ import { METRICSET_INTERVAL, METRICSET_NAME, OBSERVER_HOSTNAME, + OBSERVER_VERSION, PARENT_ID, PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -54,10 +57,7 @@ import { SavedServiceGroup, } from '../../../../common/service_groups'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; -import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { Span } from '../../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { APMDataTelemetry, APMPerService, @@ -193,17 +193,19 @@ export const tasks: TelemetryTask[] = [ size: 1, track_total_hits: false, sort: { - '@timestamp': 'desc' as const, + [AT_TIMESTAMP]: 'desc' as const, }, + fields: [AT_TIMESTAMP], }, }) - ).hits.hits[0] as { _source: { '@timestamp': string } }; + ).hits.hits[0]; if (!lastTransaction) { return {}; } - const end = new Date(lastTransaction._source['@timestamp']).getTime() - 5 * 60 * 1000; + const end = + new Date(lastTransaction.fields[AT_TIMESTAMP]![0] as string).getTime() - 5 * 60 * 1000; const start = end - 60 * 1000; @@ -512,16 +514,16 @@ export const tasks: TelemetryTask[] = [ }, }, sort: { - '@timestamp': 'asc', + [AT_TIMESTAMP]: 'asc', }, - _source: ['@timestamp'], + fields: [AT_TIMESTAMP], }, }) : null; - const event = retainmentResponse?.hits.hits[0]?._source as + const event = retainmentResponse?.hits.hits[0]?.fields as | { - '@timestamp': number; + [AT_TIMESTAMP]: number[]; } | undefined; @@ -535,7 +537,7 @@ export const tasks: TelemetryTask[] = [ ? { retainment: { [processorEvent]: { - ms: new Date().getTime() - new Date(event['@timestamp']).getTime(), + ms: new Date().getTime() - new Date(event[AT_TIMESTAMP][0]).getTime(), }, }, } @@ -690,16 +692,16 @@ export const tasks: TelemetryTask[] = [ sort: { '@timestamp': 'desc', }, + fields: asMutableArray([OBSERVER_VERSION] as const), }, }); - const hit = response.hits.hits[0]?._source as Pick; - - if (!hit || !hit.observer?.version) { + const event = unflattenKnownApmEventFields(response.hits.hits[0]?.fields); + if (!event || !event.observer?.version) { return {}; } - const [major, minor, patch] = hit.observer.version.split('.').map((part) => Number(part)); + const [major, minor, patch] = event.observer.version.split('.').map((part) => Number(part)); return { version: { diff --git a/x-pack/plugins/observability_solution/apm/server/lib/connections/get_connection_stats/get_destination_map.ts b/x-pack/plugins/observability_solution/apm/server/lib/connections/get_connection_stats/get_destination_map.ts index 4ab2e753832a4..cbcad6dea5baf 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/connections/get_connection_stats/get_destination_map.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/connections/get_connection_stats/get_destination_map.ts @@ -186,7 +186,6 @@ export const getDestinationMap = ({ }, size: destinationsBySpanId.size, fields: asMutableArray([SERVICE_NAME, SERVICE_ENVIRONMENT, AGENT_NAME, PARENT_ID] as const), - _source: false, }, }); diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_error_name.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_error_name.ts index 88d0040f70fc9..5d4977a73b42f 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_error_name.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_error_name.ts @@ -9,6 +9,10 @@ import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; import { Maybe } from '../../../typings/common'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; -export function getErrorName({ error }: { error: Maybe }): string { +export function getErrorName({ + error, +}: { + error: Maybe> & { log?: { message?: string } }; +}): string { return error?.log?.message || error?.exception?.[0]?.message || NOT_AVAILABLE_LABEL; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 96ddbe15c4287..dfc32ec9eb54e 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -184,6 +184,7 @@ export function registerTransactionDurationRuleType({ body: { track_total_hits: false, size: 0, + _source: false as const, query: { bool: { filter: [ diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts index 5f36325031ccb..7072639f8526e 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts @@ -8,6 +8,9 @@ import datemath from '@elastic/datemath'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { LogSourcesService } from '@kbn/logs-data-access-plugin/common/types'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { maybe } from '../../../../common/utils/maybe'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { flattenObject, KeyValuePair } from '../../../../common/utils/flatten_object'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import { PROCESSOR_EVENT, TRACE_ID } from '../../../../common/es_fields/apm'; @@ -86,6 +89,7 @@ export async function getLogCategories({ const rawSamplingProbability = Math.min(100_000 / totalDocCount, 1); const samplingProbability = rawSamplingProbability < 0.5 ? rawSamplingProbability : 1; + const fields = asMutableArray(['message', TRACE_ID] as const); const categorizedLogsRes = await search({ index, size: 1, @@ -108,7 +112,7 @@ export async function getLogCategories({ top_hits: { sort: { '@timestamp': 'desc' as const }, size: 1, - _source: ['message', TRACE_ID], + fields, }, }, }, @@ -120,9 +124,11 @@ export async function getLogCategories({ const promises = categorizedLogsRes.aggregations?.sampling.categories?.buckets.map( async ({ doc_count: docCount, key, sample }) => { - const hit = sample.hits.hits[0]._source as { message: string; trace?: { id: string } }; - const sampleMessage = hit?.message; - const sampleTraceId = hit?.trace?.id; + const hit = sample.hits.hits[0]; + const event = unflattenKnownApmEventFields(hit?.fields); + + const sampleMessage = event.message as string; + const sampleTraceId = event.trace?.id; const errorCategory = key as string; if (!sampleTraceId) { @@ -140,7 +146,9 @@ export async function getLogCategories({ } ); - const sampleDoc = categorizedLogsRes.hits.hits?.[0]?._source as Record; + const event = unflattenKnownApmEventFields(maybe(categorizedLogsRes.hits.hits[0])?.fields); + + const sampleDoc = event as Record; return { logCategories: await Promise.all(promises ?? []), diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts index e7f3ace07e2a1..93c55cf1a9a30 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts @@ -13,6 +13,10 @@ import moment from 'moment'; import { ESSearchRequest } from '@kbn/es-types'; import { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import { LogSourcesService } from '@kbn/logs-data-access-plugin/common/types'; +import { CONTAINER_ID } from '@kbn/apm-types'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { maybe } from '../../../../common/utils/maybe'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, @@ -79,13 +83,17 @@ async function getContainerIdFromLogs({ esClient: ElasticsearchClient; logSourcesService: LogSourcesService; }) { + const requiredFields = asMutableArray([CONTAINER_ID] as const); const index = await logSourcesService.getFlattenedLogSources(); const res = await typedSearch<{ container: { id: string } }, any>(esClient, { index, ...params, + fields: requiredFields, }); - return res.hits.hits[0]?._source?.container?.id; + const event = unflattenKnownApmEventFields(maybe(res.hits.hits[0])?.fields, requiredFields); + + return event?.container.id; } async function getContainerIdFromTraces({ @@ -95,6 +103,7 @@ async function getContainerIdFromTraces({ params: APMEventESSearchRequest['body']; apmEventClient: APMEventClient; }) { + const requiredFields = asMutableArray([CONTAINER_ID] as const); const res = await apmEventClient.search('get_container_id_from_traces', { apm: { sources: [ @@ -104,8 +113,10 @@ async function getContainerIdFromTraces({ }, ], }, - body: params, + body: { ...params, fields: requiredFields }, }); - return res.hits.hits[0]?._source.container?.id; + const event = unflattenKnownApmEventFields(maybe(res.hits.hits[0])?.fields, requiredFields); + + return event?.container.id; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_downstream_dependency_name.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_downstream_dependency_name.ts index a5b75f76ff237..d957372285b02 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_downstream_dependency_name.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_downstream_dependency_name.ts @@ -6,6 +6,9 @@ */ import { rangeQuery } from '@kbn/observability-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { maybe } from '../../../../common/utils/maybe'; import { ApmDocumentType } from '../../../../common/document_type'; import { termQuery } from '../../../../common/utils/term_query'; import { @@ -27,6 +30,7 @@ export async function getDownstreamServiceResource({ end: number; apmEventClient: APMEventClient; }) { + const requiredFields = asMutableArray([SPAN_DESTINATION_SERVICE_RESOURCE] as const); const response = await apmEventClient.search('get_error_group_main_statistics', { apm: { sources: [ @@ -50,9 +54,11 @@ export async function getDownstreamServiceResource({ ], }, }, + fields: requiredFields, }, }); - const hit = response.hits.hits[0]; - return hit?._source?.span.destination?.service.resource; + const event = unflattenKnownApmEventFields(maybe(response.hits.hits[0])?.fields, requiredFields); + + return event?.span.destination.service.resource; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts index 0168431d0ac4e..bd966c500d1bc 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts @@ -12,6 +12,10 @@ import moment from 'moment'; import { ESSearchRequest } from '@kbn/es-types'; import { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import type { LogSourcesService } from '@kbn/logs-data-access-plugin/common/types'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { SERVICE_NAME } from '@kbn/apm-types'; +import { maybe } from '../../../../common/utils/maybe'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, @@ -102,6 +106,7 @@ async function getServiceNameFromTraces({ params: APMEventESSearchRequest['body']; apmEventClient: APMEventClient; }) { + const requiredFields = asMutableArray([SERVICE_NAME] as const); const res = await apmEventClient.search('get_service_name_from_traces', { apm: { sources: [ @@ -111,8 +116,13 @@ async function getServiceNameFromTraces({ }, ], }, - body: params, + body: { + ...params, + fields: requiredFields, + }, }); - return res.hits.hits[0]?._source.service.name; + const event = unflattenKnownApmEventFields(maybe(res.hits.hits[0])?.fields, requiredFields); + + return event?.service.name; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_metadata_for_dependency.ts b/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_metadata_for_dependency.ts index ebb3d3f57d18a..5b84743064142 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_metadata_for_dependency.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_metadata_for_dependency.ts @@ -7,8 +7,14 @@ import { rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { maybe } from '../../../common/utils/maybe'; -import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/es_fields/apm'; +import { + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_SUBTYPE, + SPAN_TYPE, +} from '../../../common/es_fields/apm'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; export interface MetadataForDependencyResponse { @@ -27,6 +33,7 @@ export async function getMetadataForDependency({ start: number; end: number; }): Promise { + const fields = asMutableArray([SPAN_TYPE, SPAN_SUBTYPE] as const); const sampleResponse = await apmEventClient.search('get_metadata_for_dependency', { apm: { events: [ProcessorEvent.span], @@ -46,16 +53,17 @@ export async function getMetadataForDependency({ ], }, }, + fields, sort: { '@timestamp': 'desc', }, }, }); - const sample = maybe(sampleResponse.hits.hits[0])?._source; + const sample = unflattenKnownApmEventFields(maybe(sampleResponse.hits.hits[0])?.fields); return { - spanType: sample?.span.type, - spanSubtype: sample?.span.subtype, + spanType: sample?.span?.type, + spanSubtype: sample?.span?.subtype, }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_top_dependency_spans.ts b/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_top_dependency_spans.ts index 1c8448579806f..2a5a804d57f04 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_top_dependency_spans.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/dependencies/get_top_dependency_spans.ts @@ -8,8 +8,11 @@ import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { kqlQuery, rangeQuery, termQuery, termsQuery } from '@kbn/observability-plugin/server'; import { keyBy } from 'lodash'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { AGENT_NAME, + AT_TIMESTAMP, EVENT_OUTCOME, SERVICE_ENVIRONMENT, SERVICE_NAME, @@ -66,6 +69,19 @@ export async function getTopDependencySpans({ sampleRangeFrom?: number; sampleRangeTo?: number; }): Promise { + const topDedsRequiredFields = asMutableArray([ + SPAN_ID, + TRACE_ID, + TRANSACTION_ID, + SPAN_NAME, + SERVICE_NAME, + SERVICE_ENVIRONMENT, + AGENT_NAME, + SPAN_DURATION, + EVENT_OUTCOME, + AT_TIMESTAMP, + ] as const); + const spans = ( await apmEventClient.search('get_top_dependency_spans', { apm: { @@ -98,23 +114,18 @@ export async function getTopDependencySpans({ ], }, }, - _source: [ - SPAN_ID, - TRACE_ID, - TRANSACTION_ID, - SPAN_NAME, - SERVICE_NAME, - SERVICE_ENVIRONMENT, - AGENT_NAME, - SPAN_DURATION, - EVENT_OUTCOME, - '@timestamp', - ], + fields: topDedsRequiredFields, }, }) - ).hits.hits.map((hit) => hit._source); + ).hits.hits.map((hit) => unflattenKnownApmEventFields(hit.fields, topDedsRequiredFields)); + + const transactionIds = spans.map((span) => span.transaction.id); - const transactionIds = spans.map((span) => span.transaction!.id); + const txRequiredFields = asMutableArray([ + TRANSACTION_ID, + TRANSACTION_TYPE, + TRANSACTION_NAME, + ] as const); const transactions = ( await apmEventClient.search('get_transactions_for_dependency_spans', { @@ -129,13 +140,13 @@ export async function getTopDependencySpans({ filter: [...termsQuery(TRANSACTION_ID, ...transactionIds)], }, }, - _source: [TRANSACTION_ID, TRANSACTION_TYPE, TRANSACTION_NAME], + fields: txRequiredFields, sort: { '@timestamp': 'desc', }, }, }) - ).hits.hits.map((hit) => hit._source); + ).hits.hits.map((hit) => unflattenKnownApmEventFields(hit.fields, txRequiredFields)); const transactionsById = keyBy(transactions, (transaction) => transaction.transaction.id); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts index 1e9576ea2f7e4..3d6fa0f5a5ef6 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts @@ -7,13 +7,17 @@ import { AggregationsAggregateOrder } from '@elastic/elasticsearch/lib/api/types'; import { kqlQuery, rangeQuery, termQuery, wildcardQuery } from '@kbn/observability-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { + AT_TIMESTAMP, ERROR_CULPRIT, ERROR_EXC_HANDLED, ERROR_EXC_MESSAGE, ERROR_EXC_TYPE, ERROR_GROUP_ID, ERROR_GROUP_NAME, + ERROR_ID, ERROR_LOG_MESSAGE, SERVICE_NAME, TRACE_ID, @@ -93,6 +97,21 @@ export async function getErrorGroupMainStatistics({ ] : []; + const requiredFields = asMutableArray([ + TRACE_ID, + AT_TIMESTAMP, + ERROR_GROUP_ID, + ERROR_ID, + ] as const); + + const optionalFields = asMutableArray([ + ERROR_CULPRIT, + ERROR_LOG_MESSAGE, + ERROR_EXC_MESSAGE, + ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, + ] as const); + const response = await apmEventClient.search('get_error_group_main_statistics', { apm: { sources: [ @@ -129,16 +148,8 @@ export async function getErrorGroupMainStatistics({ sample: { top_hits: { size: 1, - _source: [ - TRACE_ID, - ERROR_LOG_MESSAGE, - ERROR_EXC_MESSAGE, - ERROR_EXC_HANDLED, - ERROR_EXC_TYPE, - ERROR_CULPRIT, - ERROR_GROUP_ID, - '@timestamp', - ], + fields: [...requiredFields, ...optionalFields], + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, ERROR_EXC_HANDLED, ERROR_EXC_TYPE], sort: { '@timestamp': 'desc', }, @@ -157,15 +168,33 @@ export async function getErrorGroupMainStatistics({ const errorGroups = response.aggregations?.error_groups.buckets.map((bucket) => { + const errorSource = + 'error' in bucket.sample.hits.hits[0]._source + ? bucket.sample.hits.hits[0]._source + : undefined; + + const event = unflattenKnownApmEventFields(bucket.sample.hits.hits[0].fields, requiredFields); + + const mergedEvent = { + ...event, + error: { + ...(event.error ?? {}), + exception: + (errorSource?.error.exception?.length ?? 0) > 1 + ? errorSource?.error.exception + : event?.error.exception && [event.error.exception], + }, + }; + return { groupId: bucket.key as string, - name: getErrorName(bucket.sample.hits.hits[0]._source), - lastSeen: new Date(bucket.sample.hits.hits[0]._source['@timestamp']).getTime(), + name: getErrorName(mergedEvent), + lastSeen: new Date(mergedEvent[AT_TIMESTAMP]).getTime(), occurrences: bucket.doc_count, - culprit: bucket.sample.hits.hits[0]._source.error.culprit, - handled: bucket.sample.hits.hits[0]._source.error.exception?.[0].handled, - type: bucket.sample.hits.hits[0]._source.error.exception?.[0].type, - traceId: bucket.sample.hits.hits[0]._source.trace?.id, + culprit: mergedEvent.error.culprit, + handled: mergedEvent.error.exception?.[0].handled, + type: mergedEvent.error.exception?.[0].type, + traceId: mergedEvent.trace?.id, }; }) ?? []; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_sample_ids.ts b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_sample_ids.ts index 0a154d3ad13fa..fc80c3f411651 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_sample_ids.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_sample_ids.ts @@ -6,6 +6,7 @@ */ import { rangeQuery, kqlQuery } from '@kbn/observability-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { ERROR_GROUP_ID, @@ -42,6 +43,7 @@ export async function getErrorGroupSampleIds({ start: number; end: number; }): Promise { + const requiredFields = asMutableArray([ERROR_ID] as const); const resp = await apmEventClient.search('get_error_group_sample_ids', { apm: { sources: [ @@ -66,7 +68,7 @@ export async function getErrorGroupSampleIds({ should: [{ term: { [TRANSACTION_SAMPLED]: true } }], // prefer error samples with related transactions }, }, - _source: [ERROR_ID, 'transaction'], + fields: requiredFields, sort: asMutableArray([ { _score: { order: 'desc' } }, // sort by _score first to ensure that errors with transaction.sampled:true ends up on top { '@timestamp': { order: 'desc' } }, // sort by timestamp to get the most recent error @@ -74,8 +76,8 @@ export async function getErrorGroupSampleIds({ }, }); const errorSampleIds = resp.hits.hits.map((item) => { - const source = item._source; - return source.error.id; + const event = unflattenKnownApmEventFields(item.fields, requiredFields); + return event.error?.id; }); return { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_sample_details.ts b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_sample_details.ts index 348949d3ecca5..91da19224d83c 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_sample_details.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_sample_details.ts @@ -6,7 +6,28 @@ */ import { rangeQuery, kqlQuery } from '@kbn/observability-plugin/server'; -import { ERROR_ID, SERVICE_NAME } from '../../../../common/es_fields/apm'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { maybe } from '../../../../common/utils/maybe'; +import { + AGENT_NAME, + AGENT_VERSION, + AT_TIMESTAMP, + ERROR_EXCEPTION, + ERROR_GROUP_ID, + ERROR_ID, + ERROR_EXC_MESSAGE, + ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, + PROCESSOR_EVENT, + PROCESSOR_NAME, + SERVICE_NAME, + TIMESTAMP_US, + TRACE_ID, + TRANSACTION_ID, + ERROR_STACK_TRACE, + SPAN_ID, +} from '../../../../common/es_fields/apm'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { ApmDocumentType } from '../../../../common/document_type'; import { RollupInterval } from '../../../../common/rollup'; @@ -17,7 +38,15 @@ import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; export interface ErrorSampleDetailsResponse { transaction: Transaction | undefined; - error: APMError; + error: Omit & { + transaction?: { id?: string; type?: string }; + error: { + id: string; + } & Omit & { + exception?: APMError['error']['exception']; + log?: APMError['error']['log']; + }; + }; } export async function getErrorSampleDetails({ @@ -36,7 +65,29 @@ export async function getErrorSampleDetails({ apmEventClient: APMEventClient; start: number; end: number; -}): Promise { +}): Promise> { + const requiredFields = asMutableArray([ + AGENT_NAME, + PROCESSOR_EVENT, + TRACE_ID, + TIMESTAMP_US, + AT_TIMESTAMP, + SERVICE_NAME, + ERROR_ID, + ERROR_GROUP_ID, + ] as const); + + const optionalFields = asMutableArray([ + TRANSACTION_ID, + SPAN_ID, + AGENT_VERSION, + PROCESSOR_NAME, + ERROR_STACK_TRACE, + ERROR_EXC_MESSAGE, + ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, + ] as const); + const params = { apm: { sources: [ @@ -60,15 +111,29 @@ export async function getErrorSampleDetails({ ], }, }, + fields: [...requiredFields, ...optionalFields], + _source: [ERROR_EXCEPTION, 'error.log'], }, }; const resp = await apmEventClient.search('get_error_sample_details', params); - const error = resp.hits.hits[0]?._source; - const transactionId = error?.transaction?.id; - const traceId = error?.trace?.id; + const hit = maybe(resp.hits.hits[0]); + + if (!hit) { + return { + transaction: undefined, + error: undefined, + }; + } + + const source = 'error' in hit._source ? hit._source : undefined; - let transaction; + const errorFromFields = unflattenKnownApmEventFields(hit.fields, requiredFields); + + const transactionId = errorFromFields.transaction?.id ?? errorFromFields.span?.id; + const traceId = errorFromFields.trace.id; + + let transaction: Transaction | undefined; if (transactionId && traceId) { transaction = await getTransaction({ transactionId, @@ -81,6 +146,20 @@ export async function getErrorSampleDetails({ return { transaction, - error, + error: { + ...errorFromFields, + processor: { + name: errorFromFields.processor.name as 'error', + event: errorFromFields.processor.event as 'error', + }, + error: { + ...errorFromFields.error, + exception: + (source?.error.exception?.length ?? 0) > 1 + ? source?.error.exception + : errorFromFields?.error.exception && [errorFromFields.error.exception], + log: source?.error?.log, + }, + }, }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/errors/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/errors/route.ts index dd262246a80b7..62d9d883ba896 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/errors/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/errors/route.ts @@ -7,6 +7,7 @@ import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; +import { notFound } from '@hapi/boom'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { ErrorDistributionResponse, getErrorDistribution } from './distribution/get_distribution'; import { environmentRt, kueryRt, rangeRt } from '../default_api_types'; @@ -205,7 +206,7 @@ const errorGroupSampleDetailsRoute = createApmServerRoute({ const { serviceName, errorId } = params.path; const { environment, kuery, start, end } = params.query; - return getErrorSampleDetails({ + const { transaction, error } = await getErrorSampleDetails({ environment, errorId, kuery, @@ -214,6 +215,12 @@ const errorGroupSampleDetailsRoute = createApmServerRoute({ start, end, }); + + if (!error) { + throw notFound(); + } + + return { error, transaction }; }, }); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/observability_solution/apm/server/routes/fleet/register_fleet_policy_callbacks.ts index 32692849d8d7c..2237548f2d325 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/fleet/register_fleet_policy_callbacks.ts @@ -90,9 +90,9 @@ function onPackagePolicyDelete({ const internalESClient = coreStart.elasticsearch.client.asInternalUser; - const [agentConfigApiKeyId] = get(packagePolicy, AGENT_CONFIG_API_KEY_PATH).split(':'); + const [agentConfigApiKeyId] = get(packagePolicy, AGENT_CONFIG_API_KEY_PATH, '').split(':'); - const [sourceMapApiKeyId] = get(packagePolicy, SOURCE_MAP_API_KEY_PATH).split(':'); + const [sourceMapApiKeyId] = get(packagePolicy, SOURCE_MAP_API_KEY_PATH, '').split(':'); logger.debug( `Deleting API keys: ${agentConfigApiKeyId}, ${sourceMapApiKeyId} (package policy: ${packagePolicy.id})` diff --git a/x-pack/plugins/observability_solution/apm/server/routes/mobile/crashes/get_crash_groups/get_crash_group_main_statistics.ts b/x-pack/plugins/observability_solution/apm/server/routes/mobile/crashes/get_crash_groups/get_crash_group_main_statistics.ts index b0d3eabe0bab2..c606a6b045a93 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/mobile/crashes/get_crash_groups/get_crash_group_main_statistics.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/mobile/crashes/get_crash_groups/get_crash_group_main_statistics.ts @@ -8,6 +8,8 @@ import { AggregationsAggregateOrder } from '@elastic/elasticsearch/lib/api/types'; import { kqlQuery, rangeQuery, termQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../../../common/utils/as_mutable_array'; import { ERROR_CULPRIT, ERROR_TYPE, @@ -19,6 +21,7 @@ import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE, + AT_TIMESTAMP, } from '../../../../../common/es_fields/apm'; import { environmentQuery } from '../../../../../common/utils/environment_query'; import { getErrorName } from '../../../../lib/helpers/get_error_name'; @@ -68,6 +71,16 @@ export async function getMobileCrashGroupMainStatistics({ ? { [maxTimestampAggKey]: sortDirection } : { _count: sortDirection }; + const requiredFields = asMutableArray([ERROR_GROUP_ID, AT_TIMESTAMP] as const); + + const optionalFields = asMutableArray([ + ERROR_CULPRIT, + ERROR_LOG_MESSAGE, + ERROR_EXC_MESSAGE, + ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, + ] as const); + const response = await apmEventClient.search('get_crash_group_main_statistics', { apm: { events: [ProcessorEvent.error], @@ -99,22 +112,15 @@ export async function getMobileCrashGroupMainStatistics({ sample: { top_hits: { size: 1, - _source: [ - ERROR_LOG_MESSAGE, - ERROR_EXC_MESSAGE, - ERROR_EXC_HANDLED, - ERROR_EXC_TYPE, - ERROR_CULPRIT, - ERROR_GROUP_ID, - '@timestamp', - ], + fields: [...requiredFields, ...optionalFields], + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, ERROR_EXC_HANDLED, ERROR_EXC_TYPE], sort: { - '@timestamp': 'desc', + [AT_TIMESTAMP]: 'desc', }, }, }, ...(sortByLatestOccurrence - ? { [maxTimestampAggKey]: { max: { field: '@timestamp' } } } + ? { [maxTimestampAggKey]: { max: { field: AT_TIMESTAMP } } } : {}), }, }, @@ -123,14 +129,34 @@ export async function getMobileCrashGroupMainStatistics({ }); return ( - response.aggregations?.crash_groups.buckets.map((bucket) => ({ - groupId: bucket.key as string, - name: getErrorName(bucket.sample.hits.hits[0]._source), - lastSeen: new Date(bucket.sample.hits.hits[0]?._source['@timestamp']).getTime(), - occurrences: bucket.doc_count, - culprit: bucket.sample.hits.hits[0]?._source.error.culprit, - handled: bucket.sample.hits.hits[0]?._source.error.exception?.[0].handled, - type: bucket.sample.hits.hits[0]?._source.error.exception?.[0].type, - })) ?? [] + response.aggregations?.crash_groups.buckets.map((bucket) => { + const errorSource = + 'error' in bucket.sample.hits.hits[0]._source + ? bucket.sample.hits.hits[0]._source + : undefined; + + const event = unflattenKnownApmEventFields(bucket.sample.hits.hits[0].fields, requiredFields); + + const mergedEvent = { + ...event, + error: { + ...(event.error ?? {}), + exception: + (errorSource?.error.exception?.length ?? 0) > 1 + ? errorSource?.error.exception + : event?.error.exception && [event.error.exception], + }, + }; + + return { + groupId: event.error?.grouping_key, + name: getErrorName(mergedEvent), + lastSeen: new Date(mergedEvent[AT_TIMESTAMP]).getTime(), + occurrences: bucket.doc_count, + culprit: mergedEvent.error.culprit, + handled: mergedEvent.error.exception?.[0].handled, + type: mergedEvent.error.exception?.[0].type, + }; + }) ?? [] ); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/mobile/errors/get_mobile_error_group_main_statistics.ts b/x-pack/plugins/observability_solution/apm/server/routes/mobile/errors/get_mobile_error_group_main_statistics.ts index f259e17d6154c..1181aa5b02870 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/mobile/errors/get_mobile_error_group_main_statistics.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/mobile/errors/get_mobile_error_group_main_statistics.ts @@ -8,7 +8,10 @@ import { AggregationsAggregateOrder } from '@elastic/elasticsearch/lib/api/types'; import { kqlQuery, rangeQuery, termQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { + AT_TIMESTAMP, ERROR_CULPRIT, ERROR_EXC_HANDLED, ERROR_EXC_MESSAGE, @@ -67,6 +70,16 @@ export async function getMobileErrorGroupMainStatistics({ ? { [maxTimestampAggKey]: sortDirection } : { _count: sortDirection }; + const requiredFields = asMutableArray([ERROR_GROUP_ID, AT_TIMESTAMP] as const); + + const optionalFields = asMutableArray([ + ERROR_CULPRIT, + ERROR_LOG_MESSAGE, + ERROR_EXC_MESSAGE, + ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, + ] as const); + const response = await apmEventClient.search('get_error_group_main_statistics', { apm: { events: [ProcessorEvent.error], @@ -100,22 +113,15 @@ export async function getMobileErrorGroupMainStatistics({ sample: { top_hits: { size: 1, - _source: [ - ERROR_LOG_MESSAGE, - ERROR_EXC_MESSAGE, - ERROR_EXC_HANDLED, - ERROR_EXC_TYPE, - ERROR_CULPRIT, - ERROR_GROUP_ID, - '@timestamp', - ], + fields: [...requiredFields, ...optionalFields], + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, ERROR_EXC_HANDLED, ERROR_EXC_TYPE], sort: { - '@timestamp': 'desc', + [AT_TIMESTAMP]: 'desc', }, }, }, ...(sortByLatestOccurrence - ? { [maxTimestampAggKey]: { max: { field: '@timestamp' } } } + ? { [maxTimestampAggKey]: { max: { field: AT_TIMESTAMP } } } : {}), }, }, @@ -124,14 +130,34 @@ export async function getMobileErrorGroupMainStatistics({ }); return ( - response.aggregations?.error_groups.buckets.map((bucket) => ({ - groupId: bucket.key as string, - name: getErrorName(bucket.sample.hits.hits[0]._source), - lastSeen: new Date(bucket.sample.hits.hits[0]?._source['@timestamp']).getTime(), - occurrences: bucket.doc_count, - culprit: bucket.sample.hits.hits[0]?._source.error.culprit, - handled: bucket.sample.hits.hits[0]?._source.error.exception?.[0].handled, - type: bucket.sample.hits.hits[0]?._source.error.exception?.[0].type, - })) ?? [] + response.aggregations?.error_groups.buckets.map((bucket) => { + const errorSource = + 'error' in bucket.sample.hits.hits[0]._source + ? bucket.sample.hits.hits[0]._source + : undefined; + + const event = unflattenKnownApmEventFields(bucket.sample.hits.hits[0].fields, requiredFields); + + const mergedEvent = { + ...event, + error: { + ...(event.error ?? {}), + exception: + (errorSource?.error.exception?.length ?? 0) > 1 + ? errorSource?.error.exception + : event?.error.exception && [event.error.exception], + }, + }; + + return { + groupId: event.error?.grouping_key, + name: getErrorName(mergedEvent), + lastSeen: new Date(mergedEvent[AT_TIMESTAMP]).getTime(), + occurrences: bucket.doc_count, + culprit: mergedEvent.error.culprit, + handled: mergedEvent.error.exception?.[0].handled, + type: mergedEvent.error.exception?.[0].type, + }; + }) ?? [] ); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/annotations/get_derived_service_annotations.ts index e766d56c44ae4..c683e308b73b8 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/annotations/get_derived_service_annotations.ts @@ -7,9 +7,12 @@ import type { ESFilter } from '@kbn/es-types'; import { rangeQuery } from '@kbn/observability-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { maybe } from '../../../../common/utils/maybe'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; import { Annotation, AnnotationType } from '../../../../common/annotations'; -import { SERVICE_NAME, SERVICE_VERSION } from '../../../../common/es_fields/apm'; +import { AT_TIMESTAMP, SERVICE_NAME, SERVICE_VERSION } from '../../../../common/es_fields/apm'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { getBackwardCompatibleDocumentTypeFilter, @@ -66,6 +69,8 @@ export async function getDerivedServiceAnnotations({ if (versions.length <= 1) { return []; } + + const requiredFields = asMutableArray([AT_TIMESTAMP] as const); const annotations = await Promise.all( versions.map(async (version) => { const response = await apmEventClient.search('get_first_seen_of_version', { @@ -83,11 +88,21 @@ export async function getDerivedServiceAnnotations({ sort: { '@timestamp': 'asc', }, + fields: requiredFields, }, }); - const firstSeen = new Date(response.hits.hits[0]._source['@timestamp']).getTime(); + const event = unflattenKnownApmEventFields( + maybe(response.hits.hits[0])?.fields, + requiredFields + ); + + const timestamp = event?.[AT_TIMESTAMP]; + if (!timestamp) { + throw new Error('First seen for version was unexpectedly undefined or null.'); + } + const firstSeen = new Date(timestamp).getTime(); if (!isFiniteNumber(firstSeen)) { throw new Error('First seen for version was unexpectedly undefined or null.'); } @@ -99,7 +114,7 @@ export async function getDerivedServiceAnnotations({ return { type: AnnotationType.VERSION, id: version, - '@timestamp': firstSeen, + [AT_TIMESTAMP]: firstSeen, text: version, }; }) diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_agent.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_agent.ts index 94c5bcbac4e66..dd272eadf57d6 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_agent.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_agent.ts @@ -7,6 +7,8 @@ import { rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { AGENT_NAME, SERVICE_NAME, @@ -16,23 +18,7 @@ import { } from '../../../common/es_fields/apm'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { getServerlessTypeFromCloudData, ServerlessType } from '../../../common/serverless'; - -interface ServiceAgent { - agent?: { - name: string; - }; - service?: { - runtime?: { - name?: string; - }; - }; - cloud?: { - provider?: string; - service?: { - name?: string; - }; - }; -} +import { maybe } from '../../../common/utils/maybe'; export interface ServiceAgentResponse { agentName?: string; @@ -51,6 +37,13 @@ export async function getServiceAgent({ start: number; end: number; }): Promise { + const fields = asMutableArray([ + AGENT_NAME, + SERVICE_RUNTIME_NAME, + CLOUD_PROVIDER, + CLOUD_SERVICE_NAME, + ] as const); + const params = { terminate_after: 1, apm: { @@ -90,6 +83,7 @@ export async function getServiceAgent({ ], }, }, + fields, sort: { _score: { order: 'desc' as const }, }, @@ -97,11 +91,14 @@ export async function getServiceAgent({ }; const response = await apmEventClient.search('get_service_agent_name', params); - if (response.hits.total.value === 0) { + const hit = maybe(response.hits.hits[0]); + if (!hit) { return {}; } - const { agent, service, cloud } = response.hits.hits[0]._source as ServiceAgent; + const event = unflattenKnownApmEventFields(hit.fields); + + const { agent, service, cloud } = event; const serverlessType = getServerlessTypeFromCloudData(cloud?.provider, cloud?.service?.name); return { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_container_metadata.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_container_metadata.ts index 400429617d803..d16910f5984fc 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_container_metadata.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_container_metadata.ts @@ -6,19 +6,20 @@ */ import { rangeQuery } from '@kbn/observability-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { CONTAINER_ID, CONTAINER_IMAGE, KUBERNETES, KUBERNETES_POD_NAME, KUBERNETES_POD_UID, -} from '../../../common/es_fields/apm'; -import { KUBERNETES_CONTAINER_NAME, - KUBERNETES_NAMESPACE, KUBERNETES_REPLICASET_NAME, KUBERNETES_DEPLOYMENT_NAME, -} from '../../../common/es_fields/infra_metrics'; + KUBERNETES_CONTAINER_ID, + KUBERNETES_NAMESPACE, +} from '../../../common/es_fields/apm'; import { Kubernetes } from '../../../typings/es_schemas/raw/fields/kubernetes'; import { maybe } from '../../../common/utils/maybe'; import { InfraMetricsClient } from '../../lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client'; @@ -51,9 +52,21 @@ export const getServiceInstanceContainerMetadata = async ({ { exists: { field: KUBERNETES_DEPLOYMENT_NAME } }, ]; + const fields = asMutableArray([ + KUBERNETES_POD_NAME, + KUBERNETES_POD_UID, + KUBERNETES_DEPLOYMENT_NAME, + KUBERNETES_CONTAINER_ID, + KUBERNETES_CONTAINER_NAME, + KUBERNETES_NAMESPACE, + KUBERNETES_REPLICASET_NAME, + KUBERNETES_DEPLOYMENT_NAME, + ] as const); + const response = await infraMetricsClient.search({ size: 1, track_total_hits: false, + fields, query: { bool: { filter: [ @@ -69,7 +82,7 @@ export const getServiceInstanceContainerMetadata = async ({ }, }); - const sample = maybe(response.hits.hits[0])?._source as ServiceInstanceContainerMetadataDetails; + const sample = unflattenKnownApmEventFields(maybe(response.hits.hits[0])?.fields); return { kubernetes: { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_metadata_details.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_metadata_details.ts index daa49d2ed59c8..3c139f2aee0de 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_metadata_details.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_instance_metadata_details.ts @@ -7,7 +7,16 @@ import { merge } from 'lodash'; import { rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { METRICSET_NAME, SERVICE_NAME, SERVICE_NODE_NAME } from '../../../common/es_fields/apm'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields'; +import { + AGENT_NAME, + AT_TIMESTAMP, + METRICSET_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + SERVICE_NODE_NAME, +} from '../../../common/es_fields/apm'; import { maybe } from '../../../common/utils/maybe'; import { getBackwardCompatibleDocumentTypeFilter, @@ -20,6 +29,13 @@ import { Container } from '../../../typings/es_schemas/raw/fields/container'; import { Kubernetes } from '../../../typings/es_schemas/raw/fields/kubernetes'; import { Host } from '../../../typings/es_schemas/raw/fields/host'; import { Cloud } from '../../../typings/es_schemas/raw/fields/cloud'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; +import { + SERVICE_METADATA_CLOUD_KEYS, + SERVICE_METADATA_CONTAINER_KEYS, + SERVICE_METADATA_INFRA_METRICS_KEYS, + SERVICE_METADATA_SERVICE_KEYS, +} from '../../../common/service_metadata'; export interface ServiceInstanceMetadataDetailsResponse { '@timestamp': string; @@ -50,6 +66,18 @@ export async function getServiceInstanceMetadataDetails({ ...rangeQuery(start, end), ]; + const requiredKeys = asMutableArray([AT_TIMESTAMP, SERVICE_NAME, AGENT_NAME] as const); + + const optionalKeys = asMutableArray([ + SERVICE_ENVIRONMENT, + ...SERVICE_METADATA_SERVICE_KEYS, + ...SERVICE_METADATA_CLOUD_KEYS, + ...SERVICE_METADATA_CONTAINER_KEYS, + ...SERVICE_METADATA_INFRA_METRICS_KEYS, + ] as const); + + const fields = [...requiredKeys, ...optionalKeys]; + async function getApplicationMetricSample() { const response = await apmEventClient.search( 'get_service_instance_metadata_details_application_metric', @@ -66,11 +94,12 @@ export async function getServiceInstanceMetadataDetails({ filter: filter.concat({ term: { [METRICSET_NAME]: 'app' } }), }, }, + fields, }, } ); - return maybe(response.hits.hits[0]?._source); + return unflattenKnownApmEventFields(maybe(response.hits.hits[0])?.fields, requiredKeys); } async function getTransactionEventSample() { @@ -85,11 +114,14 @@ export async function getServiceInstanceMetadataDetails({ terminate_after: 1, size: 1, query: { bool: { filter } }, + fields, }, } ); - return maybe(response.hits.hits[0]?._source); + return unflattenKnownApmEventFields( + maybe(response.hits.hits[0])?.fields as undefined | FlattenedApmEvent + ); } async function getTransactionMetricSample() { @@ -108,10 +140,14 @@ export async function getServiceInstanceMetadataDetails({ filter: filter.concat(getBackwardCompatibleDocumentTypeFilter(true)), }, }, + fields, }, } ); - return maybe(response.hits.hits[0]?._source); + + return unflattenKnownApmEventFields( + maybe(response.hits.hits[0])?.fields as undefined | FlattenedApmEvent + ); } // we can expect the most detail of application metrics, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_details.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_details.ts index fb44638d8a6b0..0319ae66039e5 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_details.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_details.ts @@ -7,37 +7,26 @@ import { rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields'; import { environmentQuery } from '../../../common/utils/environment_query'; import { - AGENT, - CONTAINER, - CLOUD, CLOUD_AVAILABILITY_ZONE, CLOUD_REGION, CLOUD_MACHINE_TYPE, CLOUD_SERVICE_NAME, CONTAINER_ID, - HOST, - KUBERNETES, - SERVICE, SERVICE_NAME, SERVICE_NODE_NAME, SERVICE_VERSION, FAAS_ID, FAAS_TRIGGER_TYPE, - LABEL_TELEMETRY_AUTO_VERSION, } from '../../../common/es_fields/apm'; - import { ContainerType } from '../../../common/service_metadata'; -import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { should } from './get_service_metadata_icons'; import { isOpenTelemetryAgentName, hasOpenTelemetryPrefix } from '../../../common/agent_name'; - -type ServiceMetadataDetailsRaw = Pick< - TransactionRaw, - 'service' | 'agent' | 'host' | 'container' | 'kubernetes' | 'cloud' | 'labels' ->; +import { maybe } from '../../../common/utils/maybe'; export interface ServiceMetadataDetails { service?: { @@ -112,7 +101,6 @@ export async function getServiceMetadataDetails({ body: { track_total_hits: 1, size: 1, - _source: [SERVICE, AGENT, HOST, CONTAINER, KUBERNETES, CLOUD, LABEL_TELEMETRY_AUTO_VERSION], query: { bool: { filter, should } }, aggs: { serviceVersions: { @@ -166,13 +154,17 @@ export async function getServiceMetadataDetails({ }, totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } }, }, + fields: ['*'], }, }; const response = await apmEventClient.search('get_service_metadata_details', params); - const hit = response.hits.hits[0]?._source as ServiceMetadataDetailsRaw | undefined; - if (!hit) { + const event = unflattenKnownApmEventFields( + maybe(response.hits.hits[0])?.fields as undefined | FlattenedApmEvent + ); + + if (!event) { return { service: undefined, container: undefined, @@ -180,7 +172,7 @@ export async function getServiceMetadataDetails({ }; } - const { service, agent, host, kubernetes, container, cloud, labels } = hit; + const { service, agent, host, kubernetes, container, cloud, labels } = event; const serviceMetadataDetails = { versions: response.aggregations?.serviceVersions.buckets.map((bucket) => bucket.key as string), diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_icons.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_icons.ts index 30580ddbf0ac8..ee0a857c9b719 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_icons.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_service_metadata_icons.ts @@ -7,12 +7,15 @@ import { rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields'; +import { maybe } from '../../../common/utils/maybe'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { AGENT_NAME, CLOUD_PROVIDER, CLOUD_SERVICE_NAME, CONTAINER_ID, - KUBERNETES, SERVICE_NAME, KUBERNETES_POD_NAME, HOST_OS_PLATFORM, @@ -20,14 +23,11 @@ import { AGENT_VERSION, SERVICE_FRAMEWORK_NAME, } from '../../../common/es_fields/apm'; -import { ContainerType } from '../../../common/service_metadata'; -import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; +import { ContainerType, SERVICE_METADATA_KUBERNETES_KEYS } from '../../../common/service_metadata'; import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { ServerlessType, getServerlessTypeFromCloudData } from '../../../common/serverless'; -type ServiceMetadataIconsRaw = Pick; - export interface ServiceMetadataIcons { agentName?: string; containerType?: ContainerType; @@ -61,6 +61,14 @@ export async function getServiceMetadataIcons({ }): Promise { const filter = [{ term: { [SERVICE_NAME]: serviceName } }, ...rangeQuery(start, end)]; + const fields = asMutableArray([ + CLOUD_PROVIDER, + CONTAINER_ID, + AGENT_NAME, + CLOUD_SERVICE_NAME, + ...SERVICE_METADATA_KUBERNETES_KEYS, + ] as const); + const params = { apm: { events: [ @@ -72,8 +80,8 @@ export async function getServiceMetadataIcons({ body: { track_total_hits: 1, size: 1, - _source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME, CLOUD_SERVICE_NAME], query: { bool: { filter, should } }, + fields, }, }; @@ -88,9 +96,11 @@ export async function getServiceMetadataIcons({ }; } - const { kubernetes, cloud, container, agent } = response.hits.hits[0] - ._source as ServiceMetadataIconsRaw; + const event = unflattenKnownApmEventFields( + maybe(response.hits.hits[0])?.fields as undefined | FlattenedApmEvent + ); + const { kubernetes, cloud, container, agent } = event ?? {}; let containerType: ContainerType; if (!!kubernetes) { containerType = 'Kubernetes'; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_children.ts b/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_children.ts index 3f9cf1275cace..2ff34698c20bc 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_children.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_children.ts @@ -7,6 +7,8 @@ import { rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { isEmpty } from 'lodash'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { PROCESSOR_EVENT, SPAN_ID, @@ -16,8 +18,6 @@ import { TRACE_ID, TRANSACTION_ID, } from '../../../common/es_fields/apm'; -import type { SpanRaw } from '../../../typings/es_schemas/raw/span_raw'; -import type { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; import { getBufferedTimerange } from './utils'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; @@ -39,12 +39,16 @@ async function fetchLinkedChildrenOfSpan({ end, }); + const requiredFields = asMutableArray([TRACE_ID, PROCESSOR_EVENT] as const); + const optionalFields = asMutableArray([SPAN_ID, TRANSACTION_ID] as const); + const response = await apmEventClient.search('fetch_linked_children_of_span', { apm: { events: [ProcessorEvent.span, ProcessorEvent.transaction], }, - _source: [SPAN_LINKS, TRACE_ID, SPAN_ID, PROCESSOR_EVENT, TRANSACTION_ID], + _source: [SPAN_LINKS], body: { + fields: [...requiredFields, ...optionalFields], track_total_hits: false, size: 1000, query: { @@ -58,19 +62,32 @@ async function fetchLinkedChildrenOfSpan({ }, }, }); + + const linkedChildren = response.hits.hits.map((hit) => { + const source = 'span' in hit._source ? hit._source : undefined; + const event = unflattenKnownApmEventFields(hit.fields, requiredFields); + + return { + ...event, + span: { + ...event.span, + links: source?.span?.links ?? [], + }, + }; + }); // Filter out documents that don't have any span.links that match the combination of traceId and spanId - return response.hits.hits.filter(({ _source: source }) => { - const spanLinks = source.span?.links?.filter((spanLink) => { + return linkedChildren.filter((linkedChild) => { + const spanLinks = linkedChild?.span?.links?.filter((spanLink) => { return spanLink.trace.id === traceId && (spanId ? spanLink.span.id === spanId : true); }); return !isEmpty(spanLinks); }); } -function getSpanId(source: TransactionRaw | SpanRaw) { - return source.processor.event === ProcessorEvent.span - ? (source as SpanRaw).span.id - : (source as TransactionRaw).transaction?.id; +function getSpanId( + linkedChild: Awaited>[number] +): string { + return (linkedChild.span.id ?? linkedChild.transaction?.id) as string; } export async function getSpanLinksCountById({ @@ -90,8 +107,9 @@ export async function getSpanLinksCountById({ start, end, }); - return linkedChildren.reduce>((acc, { _source: source }) => { - source.span?.links?.forEach((link) => { + + return linkedChildren.reduce>((acc, item) => { + item.span?.links?.forEach((link) => { // Ignores span links that don't belong to this trace if (link.trace.id === traceId) { acc[link.span.id] = (acc[link.span.id] || 0) + 1; @@ -122,10 +140,10 @@ export async function getLinkedChildrenOfSpan({ end, }); - return linkedChildren.map(({ _source: source }) => { + return linkedChildren.map((item) => { return { - trace: { id: source.trace.id }, - span: { id: getSpanId(source) }, + trace: { id: item.trace.id }, + span: { id: getSpanId(item) }, }; }); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_parents.ts b/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_parents.ts index 2010cd5e86f2f..59e91e0b17e6b 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_parents.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_linked_parents.ts @@ -56,7 +56,7 @@ export async function getLinkedParentsOfSpan({ }, }); - const source = response.hits.hits?.[0]?._source as TransactionRaw | SpanRaw; + const source = response.hits.hits?.[0]?._source as Pick; return source?.span?.links || []; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_span_links_details.ts b/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_span_links_details.ts index 13f47764af375..669adb1008080 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_span_links_details.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/span_links/get_span_links_details.ts @@ -7,6 +7,8 @@ import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { chunk, compact, isEmpty, keyBy } from 'lodash'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { SERVICE_NAME, SPAN_ID, @@ -25,8 +27,6 @@ import { import { Environment } from '../../../common/environment_rt'; import { SpanLinkDetails } from '../../../common/span_links'; import { SpanLink } from '../../../typings/es_schemas/raw/fields/span_links'; -import { SpanRaw } from '../../../typings/es_schemas/raw/span_raw'; -import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; import { getBufferedTimerange } from './utils'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; @@ -48,26 +48,35 @@ async function fetchSpanLinksDetails({ end, }); + const requiredFields = asMutableArray([ + TRACE_ID, + SERVICE_NAME, + AGENT_NAME, + PROCESSOR_EVENT, + ] as const); + + const requiredTxFields = asMutableArray([ + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_DURATION, + ] as const); + + const requiredSpanFields = asMutableArray([ + SPAN_ID, + SPAN_NAME, + SPAN_DURATION, + SPAN_SUBTYPE, + SPAN_TYPE, + ] as const); + + const optionalFields = asMutableArray([SERVICE_ENVIRONMENT] as const); + const response = await apmEventClient.search('get_span_links_details', { apm: { events: [ProcessorEvent.span, ProcessorEvent.transaction], }, - _source: [ - TRACE_ID, - SPAN_ID, - TRANSACTION_ID, - SERVICE_NAME, - SPAN_NAME, - TRANSACTION_NAME, - TRANSACTION_DURATION, - SPAN_DURATION, - PROCESSOR_EVENT, - SPAN_SUBTYPE, - SPAN_TYPE, - AGENT_NAME, - SERVICE_ENVIRONMENT, - ], body: { + fields: [...requiredFields, ...requiredTxFields, ...requiredSpanFields, ...optionalFields], track_total_hits: false, size: 1000, query: { @@ -106,16 +115,67 @@ async function fetchSpanLinksDetails({ const spanIdsMap = keyBy(spanLinks, 'span.id'); - return response.hits.hits.filter(({ _source: source }) => { - // The above query might return other spans from the same transaction because siblings spans share the same transaction.id - // so, if it is a span we need to guarantee that the span.id is the same as the span links ids - if (source.processor.event === ProcessorEvent.span) { - const span = source as SpanRaw; - const hasSpanId = spanIdsMap[span.span.id] || false; - return hasSpanId; - } - return true; - }); + return response.hits.hits + .filter((hit) => { + // The above query might return other spans from the same transaction because siblings spans share the same transaction.id + // so, if it is a span we need to guarantee that the span.id is the same as the span links ids + if (hit.fields[PROCESSOR_EVENT]?.[0] === ProcessorEvent.span) { + const spanLink = unflattenKnownApmEventFields(hit.fields, [ + ...requiredFields, + ...requiredSpanFields, + ]); + + const hasSpanId = Boolean(spanIdsMap[spanLink.span.id] || false); + return hasSpanId; + } + return true; + }) + .map((hit) => { + const commonEvent = unflattenKnownApmEventFields(hit.fields, requiredFields); + + const commonDetails = { + serviceName: commonEvent.service.name, + agentName: commonEvent.agent.name, + environment: commonEvent.service.environment as Environment, + transactionId: commonEvent.transaction?.id, + }; + + if (commonEvent.processor.event === ProcessorEvent.transaction) { + const event = unflattenKnownApmEventFields(hit.fields, [ + ...requiredFields, + ...requiredTxFields, + ]); + return { + traceId: event.trace.id, + spanId: event.transaction.id, + processorEvent: commonEvent.processor.event, + transactionId: event.transaction.id, + details: { + ...commonDetails, + spanName: event.transaction.name, + duration: event.transaction.duration.us, + }, + }; + } else { + const event = unflattenKnownApmEventFields(hit.fields, [ + ...requiredFields, + ...requiredSpanFields, + ]); + + return { + traceId: event.trace.id, + spanId: event.span.id, + processorEvent: commonEvent.processor.event, + details: { + ...commonDetails, + spanName: event.span.name, + duration: event.span.duration.us, + spanSubtype: event.span.subtype, + spanType: event.span.type, + }, + }; + } + }); } export async function getSpanLinksDetails({ @@ -153,39 +213,20 @@ export async function getSpanLinksDetails({ // Creates a map for all span links details found const spanLinksDetailsMap = linkedSpans.reduce>( - (acc, { _source: source }) => { - const commonDetails = { - serviceName: source.service.name, - agentName: source.agent.name, - environment: source.service.environment as Environment, - transactionId: source.transaction?.id, - }; - - if (source.processor.event === ProcessorEvent.transaction) { - const transaction = source as TransactionRaw; - const key = `${transaction.trace.id}:${transaction.transaction.id}`; + (acc, spanLink) => { + if (spanLink.processorEvent === ProcessorEvent.transaction) { + const key = `${spanLink.traceId}:${spanLink.transactionId}`; acc[key] = { - traceId: source.trace.id, - spanId: transaction.transaction.id, - details: { - ...commonDetails, - spanName: transaction.transaction.name, - duration: transaction.transaction.duration.us, - }, + traceId: spanLink.traceId, + spanId: spanLink.transactionId, + details: spanLink.details, }; } else { - const span = source as SpanRaw; - const key = `${span.trace.id}:${span.span.id}`; + const key = `${spanLink.traceId}:${spanLink.spanId}`; acc[key] = { - traceId: source.trace.id, - spanId: span.span.id, - details: { - ...commonDetails, - spanName: span.span.name, - duration: span.span.duration.us, - spanSubtype: span.span.subtype, - spanType: span.span.type, - }, + traceId: spanLink.traceId, + spanId: spanLink.spanId, + details: spanLink.details, }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/observability_solution/apm/server/routes/traces/__snapshots__/queries.test.ts.snap index d64c33a421e19..58ec97112e8f6 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/observability_solution/apm/server/routes/traces/__snapshots__/queries.test.ts.snap @@ -12,15 +12,26 @@ Object { }, "body": Object { "_source": Array [ + "error.log.message", + "error.exception.message", + "error.exception.handled", + "error.exception.type", + ], + "fields": Array [ "timestamp.us", "trace.id", - "transaction.id", - "parent.id", "service.name", "error.id", - "error.log.message", - "error.exception", "error.grouping_key", + "processor.event", + "parent.id", + "transaction.id", + "span.id", + "error.culprit", + "error.log.message", + "error.exception.message", + "error.exception.handled", + "error.exception.type", ], "query": Object { "bool": Object { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_items.ts b/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_items.ts index d38a49745653a..55fb0aab47f38 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_items.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_items.ts @@ -10,12 +10,17 @@ import { SortResults } from '@elastic/elasticsearch/lib/api/types'; import { QueryDslQueryContainer, Sort } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { rangeQuery } from '@kbn/observability-plugin/server'; -import { last } from 'lodash'; +import { last, omit } from 'lodash'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { APMConfig } from '../..'; import { AGENT_NAME, CHILD_ID, - ERROR_EXCEPTION, + ERROR_CULPRIT, + ERROR_EXC_HANDLED, + ERROR_EXC_MESSAGE, + ERROR_EXC_TYPE, ERROR_GROUP_ID, ERROR_ID, ERROR_LOG_LEVEL, @@ -37,7 +42,7 @@ import { SPAN_SUBTYPE, SPAN_SYNC, SPAN_TYPE, - TIMESTAMP, + TIMESTAMP_US, TRACE_ID, TRANSACTION_DURATION, TRANSACTION_ID, @@ -84,6 +89,26 @@ export async function getTraceItems({ const maxTraceItems = maxTraceItemsFromUrlParam ?? config.ui.maxTraceItems; const excludedLogLevels = ['debug', 'info', 'warning']; + const requiredFields = asMutableArray([ + TIMESTAMP_US, + TRACE_ID, + SERVICE_NAME, + ERROR_ID, + ERROR_GROUP_ID, + PROCESSOR_EVENT, + ] as const); + + const optionalFields = asMutableArray([ + PARENT_ID, + TRANSACTION_ID, + SPAN_ID, + ERROR_CULPRIT, + ERROR_LOG_MESSAGE, + ERROR_EXC_MESSAGE, + ERROR_EXC_HANDLED, + ERROR_EXC_TYPE, + ] as const); + const errorResponsePromise = apmEventClient.search('get_errors_docs', { apm: { sources: [ @@ -96,23 +121,14 @@ export async function getTraceItems({ body: { track_total_hits: false, size: 1000, - _source: [ - TIMESTAMP, - TRACE_ID, - TRANSACTION_ID, - PARENT_ID, - SERVICE_NAME, - ERROR_ID, - ERROR_LOG_MESSAGE, - ERROR_EXCEPTION, - ERROR_GROUP_ID, - ], query: { bool: { filter: [{ term: { [TRACE_ID]: traceId } }, ...rangeQuery(start, end)], must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, }, }, + fields: [...requiredFields, ...optionalFields], + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, ERROR_EXC_HANDLED, ERROR_EXC_TYPE], }, }); @@ -133,8 +149,32 @@ export async function getTraceItems({ const traceDocsTotal = traceResponse.total; const exceedsMax = traceDocsTotal > maxTraceItems; - const traceDocs = traceResponse.hits.map((hit) => hit._source); - const errorDocs = errorResponse.hits.hits.map((hit) => hit._source); + + const traceDocs = traceResponse.hits.map(({ hit }) => hit); + + const errorDocs = errorResponse.hits.hits.map((hit) => { + const errorSource = 'error' in hit._source ? hit._source : undefined; + + const event = unflattenKnownApmEventFields(hit.fields, requiredFields); + + const waterfallErrorEvent: WaterfallError = { + ...event, + parent: { + ...event?.parent, + id: event?.parent?.id ?? event?.span?.id, + }, + error: { + ...(event.error ?? {}), + exception: + (errorSource?.error.exception?.length ?? 0) > 1 + ? errorSource?.error.exception + : event?.error.exception && [event.error.exception], + log: errorSource?.error.log, + }, + }; + + return waterfallErrorEvent; + }); return { exceedsMax, @@ -220,41 +260,54 @@ async function getTraceDocsPerPage({ start: number; end: number; searchAfter?: SortResults; -}) { +}): Promise<{ + hits: Array<{ hit: WaterfallTransaction | WaterfallSpan; sort: SortResults | undefined }>; + total: number; +}> { const size = Math.min(maxTraceItems, MAX_ITEMS_PER_PAGE); + const requiredFields = asMutableArray([ + AGENT_NAME, + TIMESTAMP_US, + TRACE_ID, + SERVICE_NAME, + PROCESSOR_EVENT, + ] as const); + + const requiredTxFields = asMutableArray([ + TRANSACTION_ID, + TRANSACTION_DURATION, + TRANSACTION_NAME, + TRANSACTION_TYPE, + ] as const); + + const requiredSpanFields = asMutableArray([ + SPAN_ID, + SPAN_TYPE, + SPAN_NAME, + SPAN_DURATION, + ] as const); + + const optionalFields = asMutableArray([ + PARENT_ID, + SERVICE_ENVIRONMENT, + EVENT_OUTCOME, + TRANSACTION_RESULT, + FAAS_COLDSTART, + SPAN_SUBTYPE, + SPAN_ACTION, + SPAN_COMPOSITE_COUNT, + SPAN_COMPOSITE_COMPRESSION_STRATEGY, + SPAN_COMPOSITE_SUM, + SPAN_SYNC, + CHILD_ID, + ] as const); + const body = { track_total_hits: true, size, search_after: searchAfter, - _source: [ - TIMESTAMP, - TRACE_ID, - PARENT_ID, - SERVICE_NAME, - SERVICE_ENVIRONMENT, - AGENT_NAME, - EVENT_OUTCOME, - PROCESSOR_EVENT, - TRANSACTION_DURATION, - TRANSACTION_ID, - TRANSACTION_NAME, - TRANSACTION_TYPE, - TRANSACTION_RESULT, - FAAS_COLDSTART, - SPAN_ID, - SPAN_TYPE, - SPAN_SUBTYPE, - SPAN_ACTION, - SPAN_NAME, - SPAN_DURATION, - SPAN_LINKS, - SPAN_COMPOSITE_COUNT, - SPAN_COMPOSITE_COMPRESSION_STRATEGY, - SPAN_COMPOSITE_SUM, - SPAN_SYNC, - CHILD_ID, - ], + _source: [SPAN_LINKS], query: { bool: { filter: [ @@ -266,6 +319,7 @@ async function getTraceDocsPerPage({ }, }, }, + fields: [...requiredFields, ...requiredTxFields, ...requiredSpanFields, ...optionalFields], sort: [ { _score: 'asc' }, { @@ -291,7 +345,51 @@ async function getTraceDocsPerPage({ }); return { - hits: res.hits.hits, + hits: res.hits.hits.map((hit) => { + const sort = hit.sort; + const spanLinksSource = 'span' in hit._source ? hit._source.span?.links : undefined; + + if (hit.fields[PROCESSOR_EVENT]?.[0] === ProcessorEvent.span) { + const spanEvent = unflattenKnownApmEventFields(hit.fields, [ + ...requiredFields, + ...requiredSpanFields, + ]); + + const spanWaterfallEvent: WaterfallSpan = { + ...omit(spanEvent, 'child'), + processor: { + event: 'span', + }, + span: { + ...spanEvent.span, + composite: spanEvent.span.composite + ? (spanEvent.span.composite as Required['composite']) + : undefined, + links: spanLinksSource, + }, + ...(spanEvent.child ? { child: spanEvent.child as WaterfallSpan['child'] } : {}), + }; + + return { sort, hit: spanWaterfallEvent }; + } + + const txEvent = unflattenKnownApmEventFields(hit.fields, [ + ...requiredFields, + ...requiredTxFields, + ]); + const txWaterfallEvent: WaterfallTransaction = { + ...txEvent, + processor: { + event: 'transaction', + }, + span: { + ...txEvent.span, + links: spanLinksSource, + }, + }; + + return { hit: txWaterfallEvent, sort }; + }), total: res.hits.total.value, }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_samples_by_query.ts b/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_samples_by_query.ts index 033b666d0371c..dd1330dea4e48 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_samples_by_query.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/traces/get_trace_samples_by_query.ts @@ -89,10 +89,10 @@ export async function getTraceSamplesByQuery({ }, event_category_field: PROCESSOR_EVENT, query, - filter_path: 'hits.sequences.events._source.trace.id', + fields: [TRACE_ID], }) ).hits?.sequences?.flatMap((sequence) => - sequence.events.map((event) => (event._source as { trace: { id: string } }).trace.id) + sequence.events.map((event) => (event.fields as { [TRACE_ID]: [string] })[TRACE_ID][0]) ) ?? []; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/traces/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/traces/route.ts index 328201ec9d143..0814bcdc5738f 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/traces/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/traces/route.ts @@ -12,7 +12,10 @@ import { getSearchTransactionsEvents } from '../../lib/helpers/transactions'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { environmentRt, kueryRt, probabilityRt, rangeRt } from '../default_api_types'; import { getTransaction } from '../transactions/get_transaction'; -import { getRootTransactionByTraceId } from '../transactions/get_transaction_by_trace'; +import { + type TransactionDetailRedirectInfo, + getRootTransactionByTraceId, +} from '../transactions/get_transaction_by_trace'; import { getTopTracesPrimaryStats, TopTracesPrimaryStatsResponse, @@ -128,7 +131,7 @@ const rootTransactionByTraceIdRoute = createApmServerRoute({ handler: async ( resources ): Promise<{ - transaction: Transaction; + transaction?: TransactionDetailRedirectInfo; }> => { const { params: { @@ -155,7 +158,7 @@ const transactionByIdRoute = createApmServerRoute({ handler: async ( resources ): Promise<{ - transaction: Transaction; + transaction?: Transaction; }> => { const { params: { @@ -191,7 +194,7 @@ const transactionByNameRoute = createApmServerRoute({ handler: async ( resources ): Promise<{ - transaction: Transaction; + transaction?: TransactionDetailRedirectInfo; }> => { const { params: { @@ -295,7 +298,7 @@ const transactionFromTraceByIdRoute = createApmServerRoute({ query: rangeRt, }), options: { tags: ['access:apm'] }, - handler: async (resources): Promise => { + handler: async (resources): Promise => { const { params } = resources; const { path: { transactionId, traceId }, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/observability_solution/apm/server/routes/transactions/__snapshots__/queries.test.ts.snap index deb1dec096f08..c7f832fe5ca65 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/observability_solution/apm/server/routes/transactions/__snapshots__/queries.test.ts.snap @@ -11,6 +11,25 @@ Object { ], }, "body": Object { + "_source": Array [ + "span.links", + "transaction.agent.marks", + ], + "fields": Array [ + "trace.id", + "agent.name", + "processor.event", + "@timestamp", + "timestamp.us", + "service.name", + "transaction.id", + "transaction.duration.us", + "transaction.name", + "transaction.sampled", + "transaction.type", + "processor.name", + "service.language.name", + ], "query": Object { "bool": Object { "filter": Array [ @@ -311,6 +330,11 @@ Object { ], }, "body": Object { + "fields": Array [ + "transaction.id", + "trace.id", + "@timestamp", + ], "query": Object { "bool": Object { "filter": Array [ diff --git a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_span/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_span/index.ts index 4e1b7020a98a6..dc8ab0a6aab19 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_span/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_span/index.ts @@ -7,7 +7,11 @@ import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { SPAN_ID, TRACE_ID } from '../../../../common/es_fields/apm'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields'; +import { merge, omit } from 'lodash'; +import { maybe } from '../../../../common/utils/maybe'; +import { SPAN_ID, SPAN_STACKTRACE, TRACE_ID } from '../../../../common/es_fields/apm'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import { getTransaction } from '../get_transaction'; @@ -38,6 +42,8 @@ export async function getSpan({ track_total_hits: false, size: 1, terminate_after: 1, + fields: ['*'], + _source: [SPAN_STACKTRACE], query: { bool: { filter: asMutableArray([ @@ -60,5 +66,17 @@ export async function getSpan({ : undefined, ]); - return { span: spanResp.hits.hits[0]?._source, parentTransaction }; + const hit = maybe(spanResp.hits.hits[0]); + const spanFromSource = hit && 'span' in hit._source ? hit._source : undefined; + + const event = unflattenKnownApmEventFields(hit?.fields as undefined | FlattenedApmEvent); + + return { + span: event + ? merge({}, omit(event, 'span.links'), spanFromSource, { + processor: { event: 'span' as const, name: 'transaction' as const }, + }) + : undefined, + parentTransaction, + }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction/index.ts index 8854f3075e59b..8fc9d93ceff87 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction/index.ts @@ -6,7 +6,26 @@ */ import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; -import { TRACE_ID, TRANSACTION_ID } from '../../../../common/es_fields/apm'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import type { Transaction } from '@kbn/apm-types'; +import { maybe } from '../../../../common/utils/maybe'; +import { + AGENT_NAME, + PROCESSOR_EVENT, + SERVICE_NAME, + TIMESTAMP_US, + TRACE_ID, + TRANSACTION_DURATION, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_SAMPLED, + TRANSACTION_TYPE, + AT_TIMESTAMP, + PROCESSOR_NAME, + SPAN_LINKS, + TRANSACTION_AGENT_MARKS, + SERVICE_LANGUAGE_NAME, +} from '../../../../common/es_fields/apm'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import { ApmDocumentType } from '../../../../common/document_type'; @@ -24,7 +43,23 @@ export async function getTransaction({ apmEventClient: APMEventClient; start: number; end: number; -}) { +}): Promise { + const requiredFields = asMutableArray([ + TRACE_ID, + AGENT_NAME, + PROCESSOR_EVENT, + AT_TIMESTAMP, + TIMESTAMP_US, + SERVICE_NAME, + TRANSACTION_ID, + TRANSACTION_DURATION, + TRANSACTION_NAME, + TRANSACTION_SAMPLED, + TRANSACTION_TYPE, + ] as const); + + const optionalFields = asMutableArray([PROCESSOR_NAME, SERVICE_LANGUAGE_NAME] as const); + const resp = await apmEventClient.search('get_transaction', { apm: { sources: [ @@ -47,8 +82,37 @@ export async function getTransaction({ ]), }, }, + fields: [...requiredFields, ...optionalFields], + _source: [SPAN_LINKS, TRANSACTION_AGENT_MARKS], }, }); - return resp.hits.hits[0]?._source; + const hit = maybe(resp.hits.hits[0]); + + if (!hit) { + return undefined; + } + + const event = unflattenKnownApmEventFields(hit.fields, requiredFields); + + const source = + 'span' in hit._source && 'transaction' in hit._source + ? (hit._source as { + transaction: Pick['transaction'], 'marks'>; + span?: Pick['span'], 'links'>; + }) + : undefined; + + return { + ...event, + transaction: { + ...event.transaction, + marks: source?.transaction.marks, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + span: source?.span, + }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_name/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_name/index.ts index 75b4655d70117..160e3f736580a 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_name/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_name/index.ts @@ -6,11 +6,22 @@ */ import { rangeQuery } from '@kbn/observability-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { maybe } from '../../../../common/utils/maybe'; import { ApmDocumentType } from '../../../../common/document_type'; -import { SERVICE_NAME, TRANSACTION_NAME } from '../../../../common/es_fields/apm'; +import { + AT_TIMESTAMP, + SERVICE_NAME, + TRACE_ID, + TRANSACTION_DURATION, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../../common/es_fields/apm'; import { RollupInterval } from '../../../../common/rollup'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { TransactionDetailRedirectInfo } from '../get_transaction_by_trace'; export async function getTransactionByName({ transactionName, @@ -24,7 +35,17 @@ export async function getTransactionByName({ apmEventClient: APMEventClient; start: number; end: number; -}) { +}): Promise { + const requiredFields = asMutableArray([ + AT_TIMESTAMP, + TRACE_ID, + TRANSACTION_ID, + TRANSACTION_TYPE, + TRANSACTION_NAME, + TRANSACTION_DURATION, + SERVICE_NAME, + ] as const); + const resp = await apmEventClient.search('get_transaction', { apm: { sources: [ @@ -47,8 +68,9 @@ export async function getTransactionByName({ ]), }, }, + fields: requiredFields, }, }); - return resp.hits.hits[0]?._source; + return unflattenKnownApmEventFields(maybe(resp.hits.hits[0])?.fields, requiredFields); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_trace/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_trace/index.ts index d27be0489f8da..803ae19a2228e 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_trace/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/transactions/get_transaction_by_trace/index.ts @@ -7,9 +7,40 @@ import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { rangeQuery } from '@kbn/observability-plugin/server'; -import { TRACE_ID, PARENT_ID } from '../../../../common/es_fields/apm'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { maybe } from '../../../../common/utils/maybe'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { + TRACE_ID, + PARENT_ID, + AT_TIMESTAMP, + TRANSACTION_DURATION, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_TYPE, + SERVICE_NAME, +} from '../../../../common/es_fields/apm'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +export interface TransactionDetailRedirectInfo { + [AT_TIMESTAMP]: string; + trace: { + id: string; + }; + transaction: { + id: string; + type: string; + name: string; + + duration: { + us: number; + }; + }; + service: { + name: string; + }; +} + export async function getRootTransactionByTraceId({ traceId, apmEventClient, @@ -20,7 +51,19 @@ export async function getRootTransactionByTraceId({ apmEventClient: APMEventClient; start: number; end: number; -}) { +}): Promise<{ + transaction: TransactionDetailRedirectInfo | undefined; +}> { + const requiredFields = asMutableArray([ + TRACE_ID, + TRANSACTION_ID, + TRANSACTION_NAME, + AT_TIMESTAMP, + TRANSACTION_TYPE, + TRANSACTION_DURATION, + SERVICE_NAME, + ] as const); + const params = { apm: { events: [ProcessorEvent.transaction as const], @@ -45,11 +88,15 @@ export async function getRootTransactionByTraceId({ filter: [{ term: { [TRACE_ID]: traceId } }, ...rangeQuery(start, end)], }, }, + fields: requiredFields, }, }; const resp = await apmEventClient.search('get_root_transaction_by_trace_id', params); + + const event = unflattenKnownApmEventFields(maybe(resp.hits.hits[0])?.fields, requiredFields); + return { - transaction: resp.hits.hits[0]?._source, + transaction: event, }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/transactions/trace_samples/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/transactions/trace_samples/index.ts index 191250d3781ee..18dad19635333 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/transactions/trace_samples/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/transactions/trace_samples/index.ts @@ -7,7 +7,10 @@ import { Sort, QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { + AT_TIMESTAMP, SERVICE_NAME, TRACE_ID, TRANSACTION_ID, @@ -77,6 +80,8 @@ export async function getTraceSamples({ }); } + const requiredFields = asMutableArray([TRANSACTION_ID, TRACE_ID, AT_TIMESTAMP] as const); + const response = await apmEventClient.search('get_trace_samples_hits', { apm: { events: [ProcessorEvent.transaction], @@ -94,6 +99,7 @@ export async function getTraceSamples({ }, }, size: TRACE_SAMPLES_SIZE, + fields: requiredFields, sort: [ { _score: { @@ -101,7 +107,7 @@ export async function getTraceSamples({ }, }, { - '@timestamp': { + [AT_TIMESTAMP]: { order: 'desc', }, }, @@ -109,12 +115,15 @@ export async function getTraceSamples({ }, }); - const traceSamples = response.hits.hits.map((hit) => ({ - score: hit._score, - timestamp: hit._source['@timestamp'], - transactionId: hit._source.transaction.id, - traceId: hit._source.trace.id, - })); + const traceSamples = response.hits.hits.map((hit) => { + const event = unflattenKnownApmEventFields(hit.fields, requiredFields); + return { + score: hit._score, + timestamp: event[AT_TIMESTAMP], + transactionId: event.transaction.id, + traceId: event.trace.id, + }; + }); return { traceSamples }; }); diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/utils.ts b/x-pack/plugins/observability_solution/apm_data_access/server/utils.ts index 2fac072a8cdb5..71ceac4002919 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/utils.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/utils.ts @@ -15,3 +15,4 @@ export { } from './lib/helpers'; export { withApmSpan } from './utils/with_apm_span'; +export { unflattenKnownApmEventFields } from './utils/unflatten_known_fields'; diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/utils/unflatten_known_fields.test.ts b/x-pack/plugins/observability_solution/apm_data_access/server/utils/unflatten_known_fields.test.ts new file mode 100644 index 0000000000000..9dc861a5292df --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/utils/unflatten_known_fields.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { unflattenKnownApmEventFields } from './unflatten_known_fields'; + +describe('unflattenKnownApmEventFields', () => { + it('should return an empty object when input is empty', () => { + const input = {}; + const expectedOutput = {}; + expect(unflattenKnownApmEventFields(input)).toEqual(expectedOutput); + }); + + it('should correctly unflatten a simple flat input', () => { + const input = { + '@timestamp': '2024-10-10T10:10:10.000Z', + }; + const expectedOutput = { + '@timestamp': '2024-10-10T10:10:10.000Z', + }; + expect(unflattenKnownApmEventFields(input)).toEqual(expectedOutput); + }); + + it('should override unknown fields', () => { + const input = { + 'service.name': 'node-svc', + 'service.name.text': 'node-svc', + }; + const expectedOutput = { + service: { + name: 'node-svc', + }, + }; + + expect(unflattenKnownApmEventFields(input)).toEqual(expectedOutput); + }); + + it('should correctly unflatten multiple nested fields', () => { + const input = { + 'service.name': 'node-svc', + 'service.version': '1.0.0', + 'service.environment': 'production', + 'agent.name': 'nodejs', + }; + const expectedOutput = { + service: { + name: 'node-svc', + version: '1.0.0', + environment: 'production', + }, + agent: { + name: 'nodejs', + }, + }; + expect(unflattenKnownApmEventFields(input)).toEqual(expectedOutput); + }); + + it('should handle multiple values for multi-valued fields', () => { + const input = { + 'service.name': 'node-svc', + 'service.tags': ['foo', 'bar'], + }; + const expectedOutput = { + service: { + name: 'node-svc', + tags: ['foo', 'bar'], + }, + }; + expect(unflattenKnownApmEventFields(input)).toEqual(expectedOutput); + }); + + it('should correctly unflatten with empty multi-valued fields', () => { + const input = { + 'service.name': 'node-svc', + 'service.tags': [], + }; + const expectedOutput = { + service: { + name: 'node-svc', + tags: [], + }, + }; + expect(unflattenKnownApmEventFields(input)).toEqual(expectedOutput); + }); + + it('should retain unknown fields in the output', () => { + const input = { + 'service.name': 'node-svc', + 'unknown.texts': ['foo', 'bar'], + 'unknown.field': 'foo', + unknonwField: 'bar', + }; + const expectedOutput = { + service: { + name: 'node-svc', + }, + unknown: { + field: 'foo', + texts: ['foo', 'bar'], + }, + unknonwField: 'bar', + }; + expect(unflattenKnownApmEventFields(input)).toEqual(expectedOutput); + }); + + it('should correctly unflatten nested fields with mandatory field', () => { + const input = { + 'service.name': 'node-svc', + 'service.environment': undefined, + }; + + const requiredFields: ['service.name'] = ['service.name']; + + const expectedOutput = { + service: { + name: 'node-svc', + }, + }; + expect(unflattenKnownApmEventFields(input, requiredFields)).toEqual(expectedOutput); + }); + + it('should throw an exception when mandatory field is not in the input', () => { + const input = { + 'service.environment': 'PROD', + }; + + const requiredFields: ['service.name'] = ['service.name']; + + // @ts-expect-error + expect(() => unflattenKnownApmEventFields(input, requiredFields)).toThrowError( + 'Missing required fields service.name in event' + ); + }); +}); diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/utils/unflatten_known_fields.ts b/x-pack/plugins/observability_solution/apm_data_access/server/utils/unflatten_known_fields.ts new file mode 100644 index 0000000000000..b9a4322269828 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm_data_access/server/utils/unflatten_known_fields.ts @@ -0,0 +1,168 @@ +/* + * 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 { DedotObject } from '@kbn/utility-types'; +import * as APM_EVENT_FIELDS_MAP from '@kbn/apm-types/es_fields'; +import type { ValuesType } from 'utility-types'; +import { unflattenObject } from '@kbn/observability-utils/object/unflatten_object'; +import { mergePlainObjects } from '@kbn/observability-utils/object/merge_plain_objects'; +import { castArray, isArray } from 'lodash'; +import { AgentName } from '@kbn/elastic-agent-utils'; +import { EventOutcome } from '@kbn/apm-types/src/es_schemas/raw/fields'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; + +const { + CLOUD, + AGENT, + SERVICE, + ERROR_EXCEPTION, + SPAN_LINKS, + HOST, + KUBERNETES, + CONTAINER, + TIER, + INDEX, + DATA_STEAM_TYPE, + VALUE_OTEL_JVM_PROCESS_MEMORY_HEAP, + VALUE_OTEL_JVM_PROCESS_MEMORY_NON_HEAP, + SPAN_LINKS_SPAN_ID, + SPAN_LINKS_TRACE_ID, + SPAN_STACKTRACE, + ...CONCRETE_FIELDS +} = APM_EVENT_FIELDS_MAP; + +const ALL_FIELDS = Object.values(CONCRETE_FIELDS); + +const KNOWN_MULTI_VALUED_FIELDS = [ + APM_EVENT_FIELDS_MAP.CHILD_ID, + APM_EVENT_FIELDS_MAP.PROCESS_ARGS, +] as const; + +type KnownField = ValuesType; + +type KnownSingleValuedField = Exclude; +type KnownMultiValuedField = ValuesType; + +const KNOWN_SINGLE_VALUED_FIELDS = ALL_FIELDS.filter( + (field): field is KnownSingleValuedField => !KNOWN_MULTI_VALUED_FIELDS.includes(field as any) +); + +interface TypeOverrideMap { + [APM_EVENT_FIELDS_MAP.SPAN_DURATION]: number; + [APM_EVENT_FIELDS_MAP.AGENT_NAME]: AgentName; + [APM_EVENT_FIELDS_MAP.EVENT_OUTCOME]: EventOutcome; + [APM_EVENT_FIELDS_MAP.FAAS_COLDSTART]: true; + [APM_EVENT_FIELDS_MAP.TRANSACTION_DURATION]: number; + [APM_EVENT_FIELDS_MAP.TIMESTAMP_US]: number; + [APM_EVENT_FIELDS_MAP.PROCESSOR_EVENT]: ProcessorEvent; + [APM_EVENT_FIELDS_MAP.SPAN_COMPOSITE_COUNT]: number; + [APM_EVENT_FIELDS_MAP.SPAN_COMPOSITE_SUM]: number; + [APM_EVENT_FIELDS_MAP.SPAN_SYNC]: boolean; + [APM_EVENT_FIELDS_MAP.TRANSACTION_SAMPLED]: boolean; + [APM_EVENT_FIELDS_MAP.PROCESSOR_NAME]: 'transaction' | 'metric' | 'error'; + [APM_EVENT_FIELDS_MAP.HTTP_RESPONSE_STATUS_CODE]: number; + [APM_EVENT_FIELDS_MAP.PROCESS_PID]: number; + [APM_EVENT_FIELDS_MAP.OBSERVER_VERSION_MAJOR]: number; + [APM_EVENT_FIELDS_MAP.ERROR_EXC_HANDLED]: boolean; +} + +type MaybeMultiValue = T extends KnownMultiValuedField ? U[] : U; + +type TypeOfKnownField = MaybeMultiValue< + T, + T extends keyof TypeOverrideMap ? TypeOverrideMap[T] : string +>; + +type MapToSingleOrMultiValue> = { + [TKey in keyof T]: TKey extends KnownField + ? T[TKey] extends undefined + ? TypeOfKnownField | undefined + : TypeOfKnownField + : unknown; +}; + +type UnflattenedKnownFields> = DedotObject< + MapToSingleOrMultiValue +>; + +export type FlattenedApmEvent = Record; + +export type UnflattenedApmEvent = UnflattenedKnownFields; + +export function unflattenKnownApmEventFields | undefined = undefined>( + fields: T +): T extends Record ? UnflattenedKnownFields : undefined; + +export function unflattenKnownApmEventFields< + T extends Record | undefined, + U extends Array> +>( + fields: T, + required: U +): T extends Record + ? UnflattenedKnownFields & + (U extends any[] + ? UnflattenedKnownFields<{ + [TKey in ValuesType]: keyof T extends TKey ? T[TKey] : unknown[]; + }> + : {}) + : undefined; + +export function unflattenKnownApmEventFields( + hitFields?: Record, + requiredFields?: string[] +) { + if (!hitFields) { + return undefined; + } + const missingRequiredFields = + requiredFields?.filter((key) => { + const value = hitFields?.[key]; + return value === null || value === undefined || (isArray(value) && value.length === 0); + }) ?? []; + + if (missingRequiredFields.length > 0) { + throw new Error(`Missing required fields ${missingRequiredFields.join(', ')} in event`); + } + + const copy: Record = mapToSingleOrMultiValue({ + ...hitFields, + }); + + const [knownFields, unknownFields] = Object.entries(copy).reduce( + (prev, [key, value]) => { + if (ALL_FIELDS.includes(key as KnownField)) { + prev[0][key as KnownField] = value; + } else { + prev[1][key] = value; + } + return prev; + }, + [{} as Record, {} as Record] + ); + + const unflattened = mergePlainObjects( + {}, + unflattenObject(unknownFields), + unflattenObject(knownFields) + ); + + return unflattened; +} + +export function mapToSingleOrMultiValue>( + fields: T +): MapToSingleOrMultiValue { + KNOWN_SINGLE_VALUED_FIELDS.forEach((field) => { + const value = fields[field]; + if (value !== null && value !== undefined) { + fields[field as keyof T] = castArray(value)[0]; + } + }); + + return fields; +} diff --git a/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json b/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json index ea3ebf77b25be..aeeb73bee2857 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm_data_access/tsconfig.json @@ -20,6 +20,8 @@ "@kbn/apm-utils", "@kbn/core-http-server", "@kbn/security-plugin-types-server", - "@kbn/observability-utils" + "@kbn/observability-utils", + "@kbn/utility-types", + "@kbn/elastic-agent-utils" ] } diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/helpers.ts b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/helpers.ts index ff3147def0d77..84bd07dee5c47 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/helpers.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/helpers.ts @@ -70,7 +70,7 @@ export const getChartName = ( * just returns null if the color doesn't exists in the overrides. */ export const getChartColor = (seriesOverrides: SeriesOverrides | undefined, seriesId: string) => { - const rawColor: string | null = seriesOverrides + const rawColor: string | null | undefined = seriesOverrides ? get(seriesOverrides, [seriesId, 'color']) : null; if (!rawColor) { diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 40fae48cb9dc3..7df71559aa97a 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -6,6 +6,9 @@ */ import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema'; import { + HOST_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, AGENT_NAME, CLOUD_PROVIDER, CONTAINER_ID, @@ -15,9 +18,6 @@ import { ENTITY_IDENTITY_FIELDS, ENTITY_LAST_SEEN, ENTITY_TYPE, - HOST_NAME, - SERVICE_ENVIRONMENT, - SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; @@ -28,8 +28,19 @@ export const entityTypeRt = t.union([ t.literal('container'), ]); +export const entityColumnIdsRt = t.union([ + t.literal(ENTITY_DISPLAY_NAME), + t.literal(ENTITY_LAST_SEEN), + t.literal(ENTITY_TYPE), + t.literal('alertsCount'), +]); + +export type EntityColumnIds = t.TypeOf; + export type EntityType = t.TypeOf; +export const defaultEntitySortField: EntityColumnIds = 'alertsCount'; + export const MAX_NUMBER_OF_ENTITIES = 500; export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ @@ -79,6 +90,7 @@ interface BaseEntity { [ENTITY_DISPLAY_NAME]: string; [ENTITY_DEFINITION_ID]: string; [ENTITY_IDENTITY_FIELDS]: string | string[]; + alertsCount?: number; [key: string]: any; } diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts new file mode 100644 index 0000000000000..c4b48410456f8 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '@kbn/observability-shared-plugin/common'; +import { HostEntity, ServiceEntity } from '../entities'; +import { parseIdentityFieldValuesToKql } from './parse_identity_field_values_to_kql'; + +const commonEntityFields = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + alertCount: 3, +}; + +describe('parseIdentityFieldValuesToKql', () => { + it('should return the value when identityFields is a single string', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': 'service.name', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual('service.name: "my-service"'); + }); + + it('should return values when identityFields is an array of strings', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'my-service', + 'entity.type': 'service', + 'service.environment': 'staging', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual('service.name: "my-service" AND service.environment: "staging"'); + }); + + it('should return an empty string if identityFields is empty string', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': '', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual(''); + }); + it('should return an empty array if identityFields is empty array', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': [], + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual(''); + }); + + it('should ignore fields that are not present in the entity', () => { + const entity: HostEntity = { + 'entity.identityFields': ['host.name', 'foo.bar'], + 'host.name': 'my-host', + 'entity.type': 'host', + 'cloud.provider': null, + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual('host.name: "my-host"'); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts new file mode 100644 index 0000000000000..2e3f3dadd4109 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ENTITY_IDENTITY_FIELDS } from '@kbn/observability-shared-plugin/common'; +import { Entity } from '../entities'; + +type Operator = 'AND'; +export function parseIdentityFieldValuesToKql({ + entity, + operator = 'AND', +}: { + entity: Entity; + operator?: Operator; +}) { + const mapping: string[] = []; + + const identityFields = entity[ENTITY_IDENTITY_FIELDS]; + + if (identityFields) { + const fields = [identityFields].flat(); + + fields.forEach((field) => { + if (field in entity) { + mapping.push(`${[field]}: "${entity[field as keyof Entity]}"`); + } + }); + } + + return mapping.join(` ${operator} `); +} diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc index 1467d294a4f49..fc77163ae3c5f 100644 --- a/x-pack/plugins/observability_solution/inventory/kibana.jsonc +++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc @@ -16,6 +16,7 @@ "features", "unifiedSearch", "data", + "ruleRegistry", "share" ], "requiredBundles": ["kibanaReact"], diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx new file mode 100644 index 0000000000000..c60490c8a12b1 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { type KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; +import { render, screen } from '@testing-library/react'; +import { AlertsBadge } from './alerts_badge'; +import * as useKibana from '../../hooks/use_kibana'; +import { HostEntity, ServiceEntity } from '../../../common/entities'; + +describe('AlertsBadge', () => { + jest.spyOn(useKibana, 'useKibana').mockReturnValue({ + services: { + http: { + basePath: { + prepend: (path: string) => path, + }, + }, + }, + } as unknown as KibanaReactContextValue); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('render alerts badge for a host entity', () => { + const entity: HostEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'host', + 'entity.displayName': 'foo', + 'entity.identityFields': 'host.name', + 'host.name': 'foo', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 1, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'host.name: "foo"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1'); + }); + it('render alerts badge for a service entity', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'agent.name': 'node', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': 'service.name', + 'service.name': 'bar', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 5, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'service.name: "bar"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5'); + }); + it('render alerts badge for a service entity with multiple identity fields', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'agent.name': 'node', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'bar', + 'service.environment': 'prod', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 2, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'service.name: "bar" AND service.environment: "prod"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('2'); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx new file mode 100644 index 0000000000000..ba1b992ff62c1 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -0,0 +1,49 @@ +/* + * 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 rison from '@kbn/rison'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Entity } from '../../../common/entities'; +import { useKibana } from '../../hooks/use_kibana'; +import { parseIdentityFieldValuesToKql } from '../../../common/utils/parse_identity_field_values_to_kql'; + +export function AlertsBadge({ entity }: { entity: Entity }) { + const { + services: { + http: { basePath }, + }, + } = useKibana(); + + const activeAlertsHref = basePath.prepend( + `/app/observability/alerts?_a=${rison.encode({ + kuery: parseIdentityFieldValuesToKql({ entity }), + status: 'active', + })}` + ); + return ( + + + {entity.alertsCount} + + + ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx new file mode 100644 index 0000000000000..96fb8b3736ead --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx @@ -0,0 +1,113 @@ +/* + * 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 { EuiButtonIcon, EuiDataGridColumn, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + ENTITY_DISPLAY_NAME, + ENTITY_LAST_SEEN, + ENTITY_TYPE, +} from '@kbn/observability-shared-plugin/common'; + +const alertsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel', { + defaultMessage: 'Alerts', +}); + +const alertsTooltip = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip', { + defaultMessage: 'The count of the active alerts', +}); + +const entityNameLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel', { + defaultMessage: 'Entity name', +}); +const entityNameTooltip = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.entityNameTooltip', + { + defaultMessage: 'Name of the entity (entity.displayName)', + } +); + +const entityTypeLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeLabel', { + defaultMessage: 'Type', +}); +const entityTypeTooltip = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeTooltip', { + defaultMessage: 'Type of entity (entity.type)', +}); + +const entityLastSeenLabel = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenLabel', + { + defaultMessage: 'Last seen', + } +); +const entityLastSeenToolip = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenTooltip', + { + defaultMessage: 'Timestamp of last received data for entity (entity.lastSeenTimestamp)', + } +); + +const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => ( + <> + {title} + + + + +); + +export const getColumns = ({ + showAlertsColumn, +}: { + showAlertsColumn: boolean; +}): EuiDataGridColumn[] => { + return [ + ...(showAlertsColumn + ? [ + { + id: 'alertsCount', + displayAsText: alertsLabel, + isSortable: true, + display: , + initialWidth: 100, + schema: 'numeric', + }, + ] + : []), + { + id: ENTITY_DISPLAY_NAME, + // keep it for accessibility purposes + displayAsText: entityNameLabel, + display: , + isSortable: true, + }, + { + id: ENTITY_TYPE, + // keep it for accessibility purposes + displayAsText: entityTypeLabel, + display: , + isSortable: true, + }, + { + id: ENTITY_LAST_SEEN, + // keep it for accessibility purposes + displayAsText: entityLastSeenLabel, + display: ( + + ), + defaultSortDirection: 'desc', + isSortable: true, + schema: 'datetime', + }, + ]; +}; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index 8bdfa0d46627c..697bc3304753e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -5,103 +5,32 @@ * 2.0. */ import { - EuiButtonIcon, EuiDataGrid, EuiDataGridCellValueElementProps, - EuiDataGridColumn, EuiDataGridSorting, EuiLoadingSpinner, EuiText, - EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; import { last } from 'lodash'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; +import { EntityColumnIds, EntityType } from '../../../common/entities'; import { APIReturnType } from '../../api'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; +import { getColumns } from './grid_columns'; +import { AlertsBadge } from '../alerts_badge/alerts_badge'; import { EntityName } from './entity_name'; -import { EntityType } from '../../../common/entities'; import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; type LatestEntities = InventoryEntitiesAPIReturnType['entities']; -export type EntityColumnIds = - | typeof ENTITY_DISPLAY_NAME - | typeof ENTITY_LAST_SEEN - | typeof ENTITY_TYPE; - -const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => ( - <> - {title} - - - - -); - -const entityNameLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel', { - defaultMessage: 'Entity name', -}); -const entityTypeLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeLabel', { - defaultMessage: 'Type', -}); -const entityLastSeenLabel = i18n.translate( - 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenLabel', - { - defaultMessage: 'Last seen', - } -); - -const columns: EuiDataGridColumn[] = [ - { - id: ENTITY_DISPLAY_NAME, - // keep it for accessibility purposes - displayAsText: entityNameLabel, - display: ( - - ), - isSortable: true, - }, - { - id: ENTITY_TYPE, - // keep it for accessibility purposes - displayAsText: entityTypeLabel, - display: ( - - ), - isSortable: true, - }, - { - id: ENTITY_LAST_SEEN, - // keep it for accessibility purposes - displayAsText: entityLastSeenLabel, - display: ( - - ), - defaultSortDirection: 'desc', - isSortable: true, - schema: 'datetime', - }, -]; - interface Props { loading: boolean; entities: LatestEntities; @@ -125,8 +54,6 @@ export function EntitiesGrid({ onChangeSort, onFilterByType, }: Props) { - const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id)); - const onSort: EuiDataGridSorting['onSort'] = useCallback( (newSortingColumns) => { const lastItem = last(newSortingColumns); @@ -137,6 +64,19 @@ export function EntitiesGrid({ [onChangeSort] ); + const showAlertsColumn = useMemo( + () => entities?.some((entity) => entity?.alertsCount && entity?.alertsCount > 0), + [entities] + ); + + const columnVisibility = useMemo( + () => ({ + visibleColumns: getColumns({ showAlertsColumn }).map(({ id }) => id), + setVisibleColumns: () => {}, + }), + [showAlertsColumn] + ); + const renderCellValue = useCallback( ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { const entity = entities[rowIndex]; @@ -146,6 +86,9 @@ export function EntitiesGrid({ const columnEntityTableId = columnId as EntityColumnIds; switch (columnEntityTableId) { + case 'alertsCount': + return entity?.alertsCount ? : null; + case ENTITY_TYPE: const entityType = entity[columnEntityTableId]; return ( @@ -203,8 +146,8 @@ export function EntitiesGrid({ 'xpack.inventory.entitiesGrid.euiDataGrid.inventoryEntitiesGridLabel', { defaultMessage: 'Inventory entities grid' } )} - columns={columns} - columnVisibility={{ visibleColumns, setVisibleColumns }} + columns={getColumns({ showAlertsColumn })} + columnVisibility={columnVisibility} rowCount={entities.length} renderCellValue={renderCellValue} gridStyle={{ border: 'horizontal', header: 'shade' }} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts index 10ba7fbe4119e..bf72d5d7832cf 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts @@ -15,24 +15,29 @@ export const entitiesMock = [ 'entity.type': 'host', 'entity.displayName': 'Spider-Man', 'entity.id': '0', + alertsCount: 3, }, { 'entity.lastSeenTimestamp': '2024-06-16T21:48:16.259Z', 'entity.type': 'service', 'entity.displayName': 'Iron Man', 'entity.id': '1', + alertsCount: 3, }, + { 'entity.lastSeenTimestamp': '2024-04-28T03:31:57.528Z', 'entity.type': 'host', 'entity.displayName': 'Captain America', 'entity.id': '2', + alertsCount: 10, }, { 'entity.lastSeenTimestamp': '2024-05-14T11:32:04.275Z', 'entity.type': 'host', 'entity.displayName': 'Hulk', 'entity.id': '3', + alertsCount: 1, }, { 'entity.lastSeenTimestamp': '2023-12-05T13:33:54.028Z', @@ -1630,6 +1635,7 @@ export const entitiesMock = [ 'entity.displayName': 'Sed dignissim libero a diam sagittis, in convallis leo pellentesque. Cras ut sapien sed lacus scelerisque vehicula. Pellentesque at purus pulvinar, mollis justo hendrerit, pharetra purus. Morbi dapibus, augue et volutpat ultricies, neque quam sollicitudin mauris, vitae luctus ex libero id erat. Suspendisse risus lectus, scelerisque vel odio sed.', 'entity.id': '269', + alertsCount: 4, }, { 'entity.lastSeenTimestamp': '2023-10-22T13:49:53.092Z', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx index 90b6213da84a4..ee3014e990b0b 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx @@ -17,10 +17,9 @@ import { ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; -import { defaultEntityDefinitions } from '../../../common/entities'; +import { defaultEntityDefinitions, EntityColumnIds } from '../../../common/entities'; import { useInventoryParams } from '../../hooks/use_inventory_params'; import { useKibana } from '../../hooks/use_kibana'; -import { EntityColumnIds } from '../entities_grid'; const ACTIVE_COLUMNS: EntityColumnIds[] = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN]; diff --git a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx index 7af9a9fc21acc..965434eeac6d1 100644 --- a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx @@ -7,7 +7,7 @@ import { EuiDataGridSorting } from '@elastic/eui'; import React from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { EntityType } from '../../../common/entities'; +import { EntityColumnIds, EntityType } from '../../../common/entities'; import { EntitiesGrid } from '../../components/entities_grid'; import { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider'; import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async'; @@ -76,7 +76,7 @@ export function InventoryPage() { path: {}, query: { ...query, - sortField: sorting.id, + sortField: sorting.id as EntityColumnIds, sortDirection: sorting.direction, }, }); diff --git a/x-pack/plugins/observability_solution/inventory/public/plugin.ts b/x-pack/plugins/observability_solution/inventory/public/plugin.ts index 4567e8f34a94a..b6771d2f95550 100644 --- a/x-pack/plugins/observability_solution/inventory/public/plugin.ts +++ b/x-pack/plugins/observability_solution/inventory/public/plugin.ts @@ -117,7 +117,7 @@ export class InventoryPlugin defaultMessage: 'Inventory', }), euiIconType: 'logoObservability', - appRoute: '/app/observability/inventory', + appRoute: '/app/inventory', category: DEFAULT_APP_CATEGORIES.observability, visibleIn: ['sideNav', 'globalSearch'], order: 8200, diff --git a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx index d67a7250f75a5..dc7ba13451e02 100644 --- a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx @@ -8,10 +8,9 @@ import { toNumberRt } from '@kbn/io-ts-utils'; import { Outlet, createRouter } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; -import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common'; import { InventoryPageTemplate } from '../components/inventory_page_template'; import { InventoryPage } from '../pages/inventory_page'; -import { entityTypesRt } from '../../common/entities'; +import { defaultEntitySortField, entityTypesRt, entityColumnIdsRt } from '../../common/entities'; /** * The array of route definitions to be used when the application @@ -27,7 +26,7 @@ const inventoryRoutes = { params: t.type({ query: t.intersection([ t.type({ - sortField: t.string, + sortField: entityColumnIdsRt, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), pageIndex: toNumberRt, }), @@ -39,7 +38,7 @@ const inventoryRoutes = { }), defaults: { query: { - sortField: ENTITY_LAST_SEEN, + sortField: defaultEntitySortField, sortDirection: 'desc', pageIndex: '0', }, diff --git a/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts b/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts new file mode 100644 index 0000000000000..150e946fd98d6 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts @@ -0,0 +1,47 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import { InventoryRouteHandlerResources } from '../../routes/types'; + +export type AlertsClient = Awaited>; + +export async function createAlertsClient({ + plugins, + request, +}: Pick) { + const ruleRegistryPluginStart = await plugins.ruleRegistry.start(); + const alertsClient = await ruleRegistryPluginStart.getRacClientWithRequest(request); + const alertsIndices = await alertsClient.getAuthorizedAlertsIndices([ + 'logs', + 'infrastructure', + 'apm', + 'slo', + 'observability', + ]); + + if (!alertsIndices || isEmpty(alertsIndices)) { + throw Error('No alert indices exist'); + } + type RequiredParams = ESSearchRequest & { + size: number; + track_total_hits: boolean | number; + }; + + return { + search( + searchParams: TParams + ): Promise> { + return alertsClient.find({ + ...searchParams, + index: alertsIndices.join(','), + }) as Promise; + }, + }; +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts new file mode 100644 index 0000000000000..03027430116e6 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getGroupByTermsAgg } from './get_group_by_terms_agg'; +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +describe('getGroupByTermsAgg', () => { + it('should return an empty object when fields is empty', () => { + const fields: IdentityFieldsPerEntityType = new Map(); + const result = getGroupByTermsAgg(fields); + expect(result).toEqual({}); + }); + + it('should correctly generate aggregation structure for service, host, and container entity types', () => { + const fields: IdentityFieldsPerEntityType = new Map([ + ['service', ['service.name', 'service.environment']], + ['host', ['host.name']], + ['container', ['container.id', 'foo.bar']], + ]); + + const result = getGroupByTermsAgg(fields); + + expect(result).toEqual({ + service: { + composite: { + size: 500, + sources: [ + { 'service.name': { terms: { field: 'service.name' } } }, + { 'service.environment': { terms: { field: 'service.environment' } } }, + ], + }, + }, + host: { + composite: { + size: 500, + sources: [{ 'host.name': { terms: { field: 'host.name' } } }], + }, + }, + container: { + composite: { + size: 500, + sources: [ + { + 'container.id': { + terms: { field: 'container.id' }, + }, + }, + { + 'foo.bar': { terms: { field: 'foo.bar' } }, + }, + ], + }, + }, + }); + }); + it('should override maxSize when provided', () => { + const fields: IdentityFieldsPerEntityType = new Map([['host', ['host.name']]]); + const result = getGroupByTermsAgg(fields, 10); + expect(result.host.composite.size).toBe(10); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts new file mode 100644 index 0000000000000..96ab3eb24444a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts @@ -0,0 +1,26 @@ +/* + * 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 { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +export const getGroupByTermsAgg = (fields: IdentityFieldsPerEntityType, maxSize = 500) => { + return Array.from(fields).reduce((acc, [entityType, identityFields]) => { + acc[entityType] = { + composite: { + size: maxSize, + sources: identityFields.map((field) => ({ + [field]: { + terms: { + field, + }, + }, + })), + }, + }; + return acc; + }, {} as Record); +}; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts new file mode 100644 index 0000000000000..90bf2967b894d --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ContainerEntity, HostEntity, ServiceEntity } from '../../../common/entities'; +import { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '@kbn/observability-shared-plugin/common'; +import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +const commonEntityFields = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + alertCount: 3, +}; +describe('getIdentityFields', () => { + it('should return an empty Map when no entities are provided', () => { + const result = getIdentityFieldsPerEntityType([]); + expect(result.size).toBe(0); + }); + it('should return a Map with unique entity types and their respective identity fields', () => { + const serviceEntity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const hostEntity: HostEntity = { + 'entity.identityFields': ['host.name'], + 'host.name': 'my-host', + 'entity.type': 'host', + 'cloud.provider': null, + ...commonEntityFields, + }; + + const containerEntity: ContainerEntity = { + 'entity.identityFields': 'container.id', + 'host.name': 'my-host', + 'entity.type': 'container', + 'cloud.provider': null, + 'container.id': '123', + ...commonEntityFields, + }; + + const mockEntities = [serviceEntity, hostEntity, containerEntity]; + const result = getIdentityFieldsPerEntityType(mockEntities); + + expect(result.size).toBe(3); + + expect(result.get('service')).toEqual(['service.name', 'service.environment']); + expect(result.get('host')).toEqual(['host.name']); + expect(result.get('container')).toEqual(['container.id']); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts new file mode 100644 index 0000000000000..0ca4eb9d21239 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts @@ -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 { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { Entity, EntityType } from '../../../common/entities'; + +export type IdentityFieldsPerEntityType = Map; + +export const getIdentityFieldsPerEntityType = (entities: Entity[]) => { + const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map(); + + entities.forEach((entity) => + identityFieldsPerEntityType.set(entity[ENTITY_TYPE], [entity[ENTITY_IDENTITY_FIELDS]].flat()) + ); + + return identityFieldsPerEntityType; +}; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts index 853d52d8401a9..e500ce32c3cef 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts @@ -8,11 +8,13 @@ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { kqlQuery } from '@kbn/observability-utils/es/queries/kql_query'; import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, type EntityType, - Entity, + type Entity, + type EntityColumnIds, } from '../../../common/entities'; import { getEntityDefinitionIdWhereClause, getEntityTypesWhereClause } from './query_helper'; @@ -25,15 +27,18 @@ export async function getLatestEntities({ }: { inventoryEsClient: ObservabilityElasticsearchClient; sortDirection: 'asc' | 'desc'; - sortField: string; + sortField: EntityColumnIds; entityTypes?: EntityType[]; kuery?: string; }) { - const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { + // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default. + const entitiesSortField = sortField === 'alertsCount' ? ENTITY_LAST_SEEN : sortField; + + const request = { query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getEntityTypesWhereClause(entityTypes)} | ${getEntityDefinitionIdWhereClause()} - | SORT ${sortField} ${sortDirection} + | SORT ${entitiesSortField} ${sortDirection} | LIMIT ${MAX_NUMBER_OF_ENTITIES} `, filter: { @@ -41,7 +46,9 @@ export async function getLatestEntities({ filter: [...kqlQuery(kuery)], }, }, - }); + }; + + const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', request); return esqlResultToPlainObjects(latestEntitiesEsqlResponse); } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts new file mode 100644 index 0000000000000..4e6ce545a079e --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; +import { AlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; +import { getGroupByTermsAgg } from './get_group_by_terms_agg'; +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; +import { EntityType } from '../../../common/entities'; + +interface Bucket { + key: Record; + doc_count: number; +} + +type EntityTypeBucketsAggregation = Record; + +export async function getLatestEntitiesAlerts({ + alertsClient, + kuery, + identityFieldsPerEntityType, +}: { + alertsClient: AlertsClient; + kuery?: string; + identityFieldsPerEntityType: IdentityFieldsPerEntityType; +}): Promise> { + if (identityFieldsPerEntityType.size === 0) { + return []; + } + + const filter = { + size: 0, + track_total_hits: false, + query: { + bool: { + filter: [...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...kqlQuery(kuery)], + }, + }, + }; + + const response = await alertsClient.search({ + ...filter, + aggs: getGroupByTermsAgg(identityFieldsPerEntityType), + }); + + const aggregations = response.aggregations as EntityTypeBucketsAggregation; + + const alerts = Array.from(identityFieldsPerEntityType).flatMap(([entityType]) => { + const entityAggregation = aggregations?.[entityType]; + + const buckets = entityAggregation.buckets ?? []; + + return buckets.map((bucket: Bucket) => ({ + alertsCount: bucket.doc_count, + type: entityType, + ...bucket.key, + })); + }); + + return alerts; +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index beef1b068ed15..eb80f80d02730 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -8,10 +8,15 @@ import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { jsonRt } from '@kbn/io-ts-utils'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; -import { entityTypeRt } from '../../../common/entities'; +import { orderBy } from 'lodash'; +import { joinByKey } from '@kbn/observability-utils/array/join_by_key'; +import { entityTypeRt, entityColumnIdsRt, Entity } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; +import { createAlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; +import { getLatestEntitiesAlerts } from './get_latest_entities_alerts'; +import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; export const getEntityTypesRoute = createInventoryServerRoute({ endpoint: 'GET /internal/inventory/entities/types', @@ -36,7 +41,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ params: t.type({ query: t.intersection([ t.type({ - sortField: t.string, + sortField: entityColumnIdsRt, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), }), t.partial({ @@ -48,7 +53,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ options: { tags: ['access:inventory'], }, - handler: async ({ params, context, logger }) => { + handler: async ({ params, context, logger, plugins, request }) => { const coreContext = await context.core; const inventoryEsClient = createObservabilityEsClient({ client: coreContext.elasticsearch.client.asCurrentUser, @@ -58,15 +63,40 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ const { sortDirection, sortField, entityTypes, kuery } = params.query; - const latestEntities = await getLatestEntities({ - inventoryEsClient, - sortDirection, - sortField, - entityTypes, + const [alertsClient, latestEntities] = await Promise.all([ + createAlertsClient({ plugins, request }), + getLatestEntities({ + inventoryEsClient, + sortDirection, + sortField, + entityTypes, + kuery, + }), + ]); + + const identityFieldsPerEntityType = getIdentityFieldsPerEntityType(latestEntities); + + const alerts = await getLatestEntitiesAlerts({ + identityFieldsPerEntityType, + alertsClient, kuery, }); - return { entities: latestEntities }; + const joined = joinByKey( + [...latestEntities, ...alerts], + [...identityFieldsPerEntityType.values()].flat() + ).filter((entity) => entity['entity.id']); + + return { + entities: + sortField === 'alertsCount' + ? orderBy( + joined, + [(item: Entity) => item?.alertsCount === undefined, sortField], + ['asc', sortDirection] // push entities without alertsCount to the end + ) + : joined, + }; }, }); diff --git a/x-pack/plugins/observability_solution/inventory/server/types.ts b/x-pack/plugins/observability_solution/inventory/server/types.ts index 05f75561674c6..d3d5ef0fb7f60 100644 --- a/x-pack/plugins/observability_solution/inventory/server/types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/types.ts @@ -14,6 +14,10 @@ import type { DataViewsServerPluginStart, } from '@kbn/data-views-plugin/server'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { + RuleRegistryPluginStartContract, + RuleRegistryPluginSetupContract, +} from '@kbn/rule-registry-plugin/server'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ConfigSchema {} @@ -23,12 +27,14 @@ export interface InventorySetupDependencies { inference: InferenceServerSetup; dataViews: DataViewsServerPluginSetup; features: FeaturesPluginSetup; + ruleRegistry: RuleRegistryPluginSetupContract; } export interface InventoryStartDependencies { entityManager: EntityManagerServerPluginStart; inference: InferenceServerStart; dataViews: DataViewsServerPluginStart; + ruleRegistry: RuleRegistryPluginStartContract; } export interface InventoryServerSetup {} diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 6492cd51d067a..c4c8f5d3ac59d 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -46,6 +46,10 @@ "@kbn/elastic-agent-utils", "@kbn/custom-icons", "@kbn/ui-theme", + "@kbn/rison", + "@kbn/rule-registry-plugin", + "@kbn/observability-plugin", + "@kbn/rule-data-utils", "@kbn/spaces-plugin", "@kbn/cloud-plugin" ] diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/log_rate_analysis_query.ts b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/log_rate_analysis_query.ts index a23105f08cebf..4bd0b16212e11 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/log_rate_analysis_query.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/helpers/log_rate_analysis_query.ts @@ -44,8 +44,8 @@ export const getLogRateAnalysisEQQuery = ( return; } - const group: Group[] | undefined = get(alert, 'fields["kibana.alert.group"]'); - const optionalFilter: string | undefined = get(params.searchConfiguration, 'query.query'); + const group = get(alert, 'fields["kibana.alert.group"]') as Group[] | undefined; + const optionalFilter = get(params.searchConfiguration, 'query.query') as string | undefined; const groupByFilters = getGroupFilters(group); const boolQuery = buildEsQuery({ kuery: getKuery(params.criteria[0].metrics, optionalFilter), diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx index 42f3dbd6f09f7..01a1e066c4ddb 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx @@ -10,7 +10,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { FunctionComponent } from 'react'; import { - EuiAvatar, EuiCheckableCard, EuiFlexGroup, EuiFlexItem, @@ -21,12 +20,13 @@ import { useGeneratedHtmlId, useEuiTheme, EuiBadge, + EuiFlexGrid, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { useSearchParams } from 'react-router-dom-v5-compat'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { OnboardingFlowPackageList } from '../packages_list'; -import { useCustomMargin } from '../shared/use_custom_margin'; import { Category } from './types'; import { useCustomCardsForCategory } from './use_custom_cards_for_category'; import { useVirtualSearchResults } from './use_virtual_search_results'; @@ -44,51 +44,63 @@ interface UseCaseOption { export const OnboardingFlowForm: FunctionComponent = () => { const options: UseCaseOption[] = [ { - id: 'logs', + id: 'host', label: i18n.translate( - 'xpack.observability_onboarding.experimentalOnboardingFlow.euiCheckableCard.collectAndAnalyzeMyLabel', - { defaultMessage: 'Collect and analyze logs' } + 'xpack.observability_onboarding.experimentalOnboardingFlow.euiCheckableCard.hostLabel', + { defaultMessage: 'Host' } ), description: i18n.translate( - 'xpack.observability_onboarding.onboardingFlowForm.detectPatternsAndOutliersLabel', + 'xpack.observability_onboarding.onboardingFlowForm.hostDescription', { defaultMessage: - 'Detect patterns, gain insights from logs, get alerted when surpassing error thresholds', + 'Monitor your host and the services running on it, set-up SLO, get alerted, remediate performance issues', } ), - logos: ['azure', 'aws', 'nginx', 'gcp'], - showIntegrationsBadge: true, + logos: ['kubernetes', 'opentelemetry', 'apache', 'mysql'], }, { - id: 'apm', + id: 'kubernetes', label: i18n.translate( - 'xpack.observability_onboarding.experimentalOnboardingFlow.euiCheckableCard.monitorMyApplicationPerformanceLabel', - { defaultMessage: 'Monitor my application performance' } + 'xpack.observability_onboarding.experimentalOnboardingFlow.euiCheckableCard.kubernetesLabel', + { defaultMessage: 'Kubernetes' } ), description: i18n.translate( - 'xpack.observability_onboarding.onboardingFlowForm.captureAndAnalyzeDistributedLabel', + 'xpack.observability_onboarding.onboardingFlowForm.kubernetesDescription', { defaultMessage: - 'Catch application problems, get alerted on performance issues or SLO breaches, expedite root cause analysis and remediation', + 'Observe your Kubernetes cluster, and your container workloads using logs, metrics, traces and profiling data', } ), - logos: ['opentelemetry', 'java', 'javascript', 'dotnet'], + logos: ['kubernetes', 'opentelemetry'], }, { - id: 'infra', + id: 'application', label: i18n.translate( - 'xpack.observability_onboarding.experimentalOnboardingFlow.euiCheckableCard.monitorMyInfrastructureLabel', - { defaultMessage: 'Monitor infrastructure' } + 'xpack.observability_onboarding.experimentalOnboardingFlow.euiCheckableCard.applicationLabel', + { defaultMessage: 'Application' } ), description: i18n.translate( - 'xpack.observability_onboarding.onboardingFlowForm.builtOnPowerfulElasticsearchLabel', + 'xpack.observability_onboarding.onboardingFlowForm.applicationDescription', { defaultMessage: - 'Check my system’s health, get alerted on performance issues or SLO breaches, expedite root cause analysis and remediation', + 'Monitor the frontend and backend application that you have developed, set-up synthetic monitors', } ), - logos: ['kubernetes', 'prometheus', 'docker', 'opentelemetry'], - showIntegrationsBadge: true, + logos: ['opentelemetry', 'java', 'javascript', 'dotnet'], + }, + { + id: 'cloud', + label: i18n.translate( + 'xpack.observability_onboarding.experimentalOnboardingFlow.euiCheckableCard.cloudLabel', + { defaultMessage: 'Cloud' } + ), + description: i18n.translate( + 'xpack.observability_onboarding.onboardingFlowForm.cloudDescription', + { + defaultMessage: 'Ingest telemetry data from the Cloud for your applications and services', + } + ), + logos: ['azure', 'aws', 'gcp'], }, ]; @@ -97,7 +109,6 @@ export const OnboardingFlowForm: FunctionComponent = () => { context: { isCloud }, }, } = useKibana(); - const customMargin = useCustomMargin(); const radioGroupId = useGeneratedHtmlId({ prefix: 'onboardingCategory' }); const categorySelectorTitleId = useGeneratedHtmlId(); const packageListTitleId = useGeneratedHtmlId(); @@ -152,33 +163,72 @@ export const OnboardingFlowForm: FunctionComponent = () => { return ( - - - + + + {i18n.translate( + 'xpack.observability_onboarding.experimentalOnboardingFlow.strong.startCollectingYourDataLabel', + { + defaultMessage: 'What do you want to monitor?', + } + )} + + + + {options.map((option) => ( {option.label}} + label={ + <> + + {option.label} + + {/* The description and logo icons are passed into `label` prop instead of `children` to ensure they are clickable */} + + + {option.description} + + {(option.logos || option.showIntegrationsBadge) && ( + <> + + + {option.logos?.map((logo) => ( + + + + ))} + {option.showIntegrationsBadge && ( + + + + )} + + + )} + + } checked={option.id === searchParams.get('category')} /** * onKeyDown and onKeyUp handlers disable @@ -204,54 +254,62 @@ export const OnboardingFlowForm: FunctionComponent = () => { ); } }} - > - - {option.description} - - {(option.logos || option.showIntegrationsBadge) && ( - <> - - - {option.logos?.map((logo) => ( - - - - ))} - {option.showIntegrationsBadge && ( - - - - )} - - - )} - + css={css` + flex-grow: 1; + + & > .euiPanel { + display: flex; + + & > .euiCheckableCard__label { + display: flex; + flex-direction: column; + } + } + `} + /> ))} - + {/* Hiding element instead of not rending these elements in order to preload available packages on page load */}