diff --git a/.eslintrc.js b/.eslintrc.js index 730c9599f23f9..e2d02c33288a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1995,9 +1995,6 @@ module.exports = { // logsShared depends on o11y/private plugins, but platform plugins depend on it 'x-pack/plugins/observability_solution/logs_shared/**', - // this plugin depends on visTypeTimeseries plugin (for TSVB viz) which is platform/private ATM - 'x-pack/plugins/observability_solution/infra/**', - // TODO @kibana/operations 'scripts/create_observability_rules.js', // is importing "@kbn/observability-alerting-test-data" (observability/private) 'src/cli_setup/**', // is importing "@kbn/interactive-setup-plugin" (platform/private) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ef265cf7c569a..f0d509e283b2a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -324,7 +324,7 @@ packages/kbn-custom-icons @elastic/obs-ux-logs-team packages/kbn-custom-integrations @elastic/obs-ux-logs-team packages/kbn-cypress-config @elastic/kibana-operations packages/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-discovery -packages/kbn-data-stream-adapter @elastic/security-threat-hunting-explore +packages/kbn-data-stream-adapter @elastic/security-threat-hunting packages/kbn-data-view-utils @elastic/kibana-data-discovery packages/kbn-datemath @elastic/kibana-data-discovery packages/kbn-dev-cli-errors @elastic/kibana-operations @@ -380,6 +380,7 @@ packages/kbn-i18n @elastic/kibana-core packages/kbn-i18n-react @elastic/kibana-core packages/kbn-import-locator @elastic/kibana-operations packages/kbn-import-resolver @elastic/kibana-operations +packages/kbn-index-adapter @elastic/security-threat-hunting packages/kbn-interpreter @elastic/kibana-visualizations packages/kbn-investigation-shared @elastic/obs-ux-management-team packages/kbn-io-ts-utils @elastic/obs-knowledge-team diff --git a/api_docs/kbn_elastic_assistant_common.devdocs.json b/api_docs/kbn_elastic_assistant_common.devdocs.json index 3a8972f18fd3a..fbd6ee4384bb0 100644 --- a/api_docs/kbn_elastic_assistant_common.devdocs.json +++ b/api_docs/kbn_elastic_assistant_common.devdocs.json @@ -997,7 +997,7 @@ "\nInterface for features available to the elastic assistant" ], "signature": [ - "{ readonly assistantKnowledgeBaseByDefault: boolean; readonly assistantModelEvaluation: boolean; }" + "{ readonly assistantModelEvaluation: boolean; }" ], "path": "x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts", "deprecated": false, @@ -2772,7 +2772,7 @@ "label": "GetCapabilitiesResponse", "description": [], "signature": [ - "{ assistantKnowledgeBaseByDefault: boolean; assistantModelEvaluation: boolean; }" + "{ assistantModelEvaluation: boolean; }" ], "path": "x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts", "deprecated": false, @@ -4767,7 +4767,7 @@ "\nDefault features available to the elastic assistant" ], "signature": [ - "{ readonly assistantKnowledgeBaseByDefault: true; readonly assistantModelEvaluation: false; }" + "{ readonly assistantModelEvaluation: false; }" ], "path": "x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts", "deprecated": false, @@ -5232,7 +5232,7 @@ "label": "GetCapabilitiesResponse", "description": [], "signature": [ - "Zod.ZodObject<{ assistantKnowledgeBaseByDefault: Zod.ZodBoolean; assistantModelEvaluation: Zod.ZodBoolean; }, \"strip\", Zod.ZodTypeAny, { assistantKnowledgeBaseByDefault: boolean; assistantModelEvaluation: boolean; }, { assistantKnowledgeBaseByDefault: boolean; assistantModelEvaluation: boolean; }>" + "Zod.ZodObject<{ assistantModelEvaluation: Zod.ZodBoolean; }, \"strip\", Zod.ZodTypeAny, { assistantModelEvaluation: boolean; }, { assistantModelEvaluation: boolean; }>" ], "path": "x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts", "deprecated": false, @@ -6201,4 +6201,4 @@ } ] } -} \ No newline at end of file +} diff --git a/api_docs/security_solution.devdocs.json b/api_docs/security_solution.devdocs.json index 608baf2669dfc..94910aa77b079 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -420,7 +420,7 @@ "\nExperimental flag needed to enable the link" ], "signature": [ - "\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined" + "\"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined" ], "path": "x-pack/plugins/security_solution/public/common/links/types.ts", "deprecated": false, @@ -500,7 +500,7 @@ "\nExperimental flag needed to disable the link. Opposite of experimentalKey" ], "signature": [ - "\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined" + "\"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined" ], "path": "x-pack/plugins/security_solution/public/common/links/types.ts", "deprecated": false, @@ -1791,7 +1791,7 @@ "label": "experimentalFeatures", "description": [], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }" + "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/public/types.ts", "deprecated": false, @@ -3039,7 +3039,7 @@ "\nThe security solution generic experimental features" ], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }" + "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/server/plugin_contract.ts", "deprecated": false, @@ -3212,7 +3212,7 @@ "label": "ExperimentalFeatures", "description": [], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }" + "{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }" ], "path": "x-pack/plugins/security_solution/common/experimental_features.ts", "deprecated": false, @@ -3278,7 +3278,7 @@ "\nA list of allowed values that can be used in `xpack.securitySolution.enableExperimental`.\nThis object is then used to validate and parse the value entered." ], "signature": [ - "{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: true; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesDisabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly assistantKnowledgeBaseByDefault: true; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly graphVisualizationInFlyoutEnabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreDisabled: false; readonly siemMigrationsEnabled: false; }" + "{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: true; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesDisabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly graphVisualizationInFlyoutEnabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreDisabled: false; readonly siemMigrationsEnabled: false; }" ], "path": "x-pack/plugins/security_solution/common/experimental_features.ts", "deprecated": false, @@ -3287,4 +3287,4 @@ } ] } -} \ No newline at end of file +} diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 1357af980d278..e3b01ba0afcf7 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -8,6 +8,12 @@ Actions are instantiations of a connector that are linked to rules and run as ba [cols="2"] |=== +// ifeval::["featureAIConnector"=="true"] +// a| <> + +// | Send a request to {infer}. +// endif::[] + a| <> | Send a request to {bedrock}. @@ -28,10 +34,6 @@ a| <> | Send a request to {gemini}. -a| <> - -| Send a request to {infer}. - a| <> | Send email from your server. diff --git a/docs/management/connectors/action-types/inference.asciidoc b/docs/management/connectors/action-types/inference.asciidoc index ea8a0be675e18..d47374e9b4cdd 100644 --- a/docs/management/connectors/action-types/inference.asciidoc +++ b/docs/management/connectors/action-types/inference.asciidoc @@ -1,13 +1,14 @@ [[inference-action-type]] -== {infer-cap} connector and action +== AI connector and action ++++ -{infer-cap} +AI ++++ :frontmatter-description: Add a connector that can send requests to {inference}. :frontmatter-tags-products: [kibana] :frontmatter-tags-content-type: [how-to] :frontmatter-tags-user-goals: [configure] +coming::[] The {infer} connector uses the {es} client to send requests to an {infer} service. The connector uses the <> to send the request. @@ -16,6 +17,7 @@ The connector uses the <> to send the r [[define-inference-ui]] === Create connectors in {kib} +// TBD After you set the `xpack.stack_connectors.enableExperimental` to include `inferenceConnectorOn`, You can create connectors in *{stack-manage-app} > {connectors-ui}*. For example: [role="screenshot"] diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index c5233ad4f4934..c5c1ce4600c5d 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -4,7 +4,9 @@ include::action-types/crowdstrike.asciidoc[leveloffset=+1] include::action-types/d3security.asciidoc[leveloffset=+1] include::action-types/email.asciidoc[leveloffset=+1] include::action-types/gemini.asciidoc[leveloffset=+1] -include::action-types/inference.asciidoc[leveloffset=+1] +// ifeval::["featureAIConnector"=="true"] +// include::action-types/inference.asciidoc[leveloffset=+1] +// endif::[] include::action-types/resilient.asciidoc[leveloffset=+1] include::action-types/index.asciidoc[leveloffset=+1] include::action-types/jira.asciidoc[leveloffset=+1] diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index e0fa3f0aab860..0f4987822dc32 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -269,7 +269,6 @@ A configuration URL that varies by connector: -- * For an <>, specifies the {bedrock} request URL. * For an <>, specifies the {gemini} request URL. -* For an <>, specifies the Elastic {inference} request. * For a <>, specifies the OpenAI request URL. * For a <>, specifies the {ibm-r} instance URL. * For a <>, specifies the Jira instance URL. @@ -277,7 +276,9 @@ A configuration URL that varies by connector: * For a <>, specifies the PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. * For a <>, <>, or <> specifies the ServiceNow instance URL. * For a <>, specifies the {swimlane} instance URL. - +// ifeval::["featureAIConnector"=="true"] +// * For an <>, specifies the Elastic {inference} request. +// endif::[] NOTE: If you are using the `xpack.actions.allowedHosts` setting, make sure the hostname in the URL is added to the allowed hosts. -- diff --git a/oas_docs/examples/get_connector_types_generativeai_response.yaml b/oas_docs/examples/get_connector_types_generativeai_response.yaml index 8299da3558150..a97199e0a3927 100644 --- a/oas_docs/examples/get_connector_types_generativeai_response.yaml +++ b/oas_docs/examples/get_connector_types_generativeai_response.yaml @@ -31,14 +31,3 @@ value: supported_feature_ids: - generativeAIForSecurity is_system_action_type: false - - id: .inference - name: Inference API - enabled: true - enabled_in_config: true - enabled_in_license: true - minimum_license_required: enterprise - supported_feature_ids: - - generativeAIForSecurity - - generativeAIForObservability - - generativeAIForSearchPlayground - is_system_action_type: false diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 5f18154db449d..3205dfac4a72e 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -36267,7 +36267,7 @@ paths: - $ref: '#/components/parameters/SLOs_space_id' - $ref: '#/components/parameters/SLOs_slo_id' responses: - '204': + '200': content: application/json; Elastic-Api-Version=2023-10-31: schema: @@ -36311,7 +36311,7 @@ paths: - $ref: '#/components/parameters/SLOs_space_id' - $ref: '#/components/parameters/SLOs_slo_id' responses: - '200': + '204': description: Successful request '400': content: @@ -39976,17 +39976,20 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: >- + #/components/schemas/Security_Detections_API_AlertSuppressionDurationUnit value: minimum: 1 type: integer required: - value - unit + Security_Detections_API_AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string Security_Detections_API_AlertSuppressionGroupBy: items: type: string diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 133dede5fcd0c..3aca8b845d53d 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -40728,7 +40728,7 @@ paths: - $ref: '#/components/parameters/SLOs_space_id' - $ref: '#/components/parameters/SLOs_slo_id' responses: - '204': + '200': content: application/json; Elastic-Api-Version=2023-10-31: schema: @@ -40772,7 +40772,7 @@ paths: - $ref: '#/components/parameters/SLOs_space_id' - $ref: '#/components/parameters/SLOs_slo_id' responses: - '200': + '204': description: Successful request '400': content: @@ -48220,17 +48220,20 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: >- + #/components/schemas/Security_Detections_API_AlertSuppressionDurationUnit value: minimum: 1 type: integer required: - value - unit + Security_Detections_API_AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string Security_Detections_API_AlertSuppressionGroupBy: items: type: string diff --git a/oas_docs/overlays/connectors.overlays.yaml b/oas_docs/overlays/connectors.overlays.yaml index 022946e893be2..816542a450e3a 100644 --- a/oas_docs/overlays/connectors.overlays.yaml +++ b/oas_docs/overlays/connectors.overlays.yaml @@ -140,6 +140,8 @@ actions: default: {} description: The connector configuration details. oneOf: + # AI (.inference) TBD + # - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/inference_config.yaml' # Bedrock (.bedrock) - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/bedrock_config.yaml' # Crowdstrike (.crowdstrike) @@ -194,6 +196,8 @@ actions: additionalProperties: {} default: {} oneOf: + # AI (.inference) + # - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/inference_secrets.yaml' # Bedrock (.bedrock) - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/bedrock_secrets.yaml' # Crowdstrike (.crowdstrike) @@ -253,6 +257,8 @@ actions: default: {} description: The connector configuration details. oneOf: + # AI (.inference) + # - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/inference_config.yaml' # Bedrock (.bedrock) - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/bedrock_config.yaml' # Crowdstrike (.crowdstrike) @@ -307,6 +313,8 @@ actions: additionalProperties: {} default: {} oneOf: + # AI (.inference) + # - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/inference_secrets.yaml' # Bedrock (.bedrock) - $ref: '../../x-pack/plugins/actions/docs/openapi/components/schemas/bedrock_secrets.yaml' # Crowdstrike (.crowdstrike) diff --git a/package.json b/package.json index 87905c955d2d8..48e377c9a6237 100644 --- a/package.json +++ b/package.json @@ -567,6 +567,7 @@ "@kbn/i18n-react": "link:packages/kbn-i18n-react", "@kbn/iframe-embedded-plugin": "link:x-pack/test/functional_embedded/plugins/iframe_embedded", "@kbn/image-embeddable-plugin": "link:src/plugins/image_embeddable", + "@kbn/index-adapter": "link:packages/kbn-index-adapter", "@kbn/index-lifecycle-management-common-shared": "link:x-pack/packages/index-lifecycle-management/index_lifecycle_management_common_shared", "@kbn/index-lifecycle-management-plugin": "link:x-pack/plugins/index_lifecycle_management", "@kbn/index-management-plugin": "link:x-pack/plugins/index_management", diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts index db95dcf4155bc..6da1decaab9ab 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts @@ -58,6 +58,8 @@ export class K8sEntity extends Serializable { 'entity.definition_id': `builtin_${entityTypeWithSchema}`, 'entity.identity_fields': identityFields, 'entity.display_name': getDisplayName({ identityFields, fields }), + 'entity.definition_version': '1.0.0', + 'entity.schema_version': '1.0', }); } } diff --git a/packages/kbn-code-owners/src/file_code_owner.ts b/packages/kbn-code-owners/src/file_code_owner.ts index 1f98d50c4bacc..a7812a6121d90 100644 --- a/packages/kbn-code-owners/src/file_code_owner.ts +++ b/packages/kbn-code-owners/src/file_code_owner.ts @@ -39,7 +39,9 @@ export function getPathsWithOwnersReversed(): PathWithOwners[] { const codeownersLines = codeownersContent.split(/\r?\n/); const codeowners = codeownersLines .map((line) => line.trim()) - .filter((line) => line && line[0] !== '#'); + .filter((line) => line && line[0] !== '#') + // kibanamachine is an assignment override on backport branches to avoid review requests + .filter((line) => line && !line.includes('@kibanamachine')); const pathsWithOwners: PathWithOwners[] = codeowners.map((c) => { const [path, ...ghTeams] = c.split(/\s+/); diff --git a/packages/kbn-data-stream-adapter/index.ts b/packages/kbn-data-stream-adapter/index.ts index 4fd7c7ebd1572..f03a384dca1ff 100644 --- a/packages/kbn-data-stream-adapter/index.ts +++ b/packages/kbn-data-stream-adapter/index.ts @@ -9,13 +9,13 @@ export { DataStreamAdapter } from './src/data_stream_adapter'; export { DataStreamSpacesAdapter } from './src/data_stream_spaces_adapter'; -export { retryTransientEsErrors } from './src/retry_transient_es_errors'; -export { ecsFieldMap, type EcsFieldMap } from './src/field_maps/ecs_field_map'; +export { retryTransientEsErrors, ecsFieldMap } from '@kbn/index-adapter'; export type { - DataStreamAdapterParams, SetComponentTemplateParams, SetIndexTemplateParams, InstallParams, -} from './src/data_stream_adapter'; -export * from './src/field_maps/types'; + EcsFieldMap, +} from '@kbn/index-adapter'; + +export * from '@kbn/index-adapter/src/field_maps/types'; diff --git a/packages/kbn-data-stream-adapter/kibana.jsonc b/packages/kbn-data-stream-adapter/kibana.jsonc index 99cbb458a8517..43317dca0b91e 100644 --- a/packages/kbn-data-stream-adapter/kibana.jsonc +++ b/packages/kbn-data-stream-adapter/kibana.jsonc @@ -1,5 +1,6 @@ { - "type": "shared-common", + "type": "shared-server", "id": "@kbn/data-stream-adapter", - "owner": "@elastic/security-threat-hunting-explore" + "owner": "@elastic/security-threat-hunting", + "visibility": "shared" } diff --git a/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.test.ts b/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.test.ts index 97ca06b04ac83..e2141d4afb740 100644 --- a/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.test.ts +++ b/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.test.ts @@ -136,10 +136,11 @@ describe('createOrUpdateDataStream', () => { it(`should create data stream if not exists`, async () => { esClient.indices.getDataStream.mockResolvedValueOnce({ data_streams: [] }); - await createDataStream({ + await createOrUpdateDataStream({ esClient, logger, name, + totalFieldsLimit, }); expect(esClient.indices.createDataStream).toHaveBeenCalledWith({ name }); diff --git a/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.ts b/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.ts index 791c99c6e3809..2b0fba3fb0ac0 100644 --- a/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.ts +++ b/packages/kbn-data-stream-adapter/src/create_or_update_data_stream.ts @@ -11,7 +11,7 @@ import type { IndicesDataStream } from '@elastic/elasticsearch/lib/api/types'; import type { IndicesSimulateIndexTemplateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Logger, ElasticsearchClient } from '@kbn/core/server'; import { get } from 'lodash'; -import { retryTransientEsErrors } from './retry_transient_es_errors'; +import { retryTransientEsErrors } from '@kbn/index-adapter'; interface UpdateIndexMappingsOpts { logger: Logger; @@ -168,7 +168,7 @@ export async function createDataStream({ esClient, name, }: CreateDataStreamParams): Promise { - logger.info(`Creating data stream - ${name}`); + logger.debug(`Checking data stream exists - ${name}`); // check if data stream exists let dataStreamExists = false; @@ -189,6 +189,7 @@ export async function createDataStream({ if (dataStreamExists) { return; } + logger.info(`Installing data stream - ${name}`); try { await retryTransientEsErrors(() => esClient.indices.createDataStream({ name }), { logger }); diff --git a/packages/kbn-data-stream-adapter/src/data_stream_adapter.ts b/packages/kbn-data-stream-adapter/src/data_stream_adapter.ts index 6843c181b2638..f54ed81312d75 100644 --- a/packages/kbn-data-stream-adapter/src/data_stream_adapter.ts +++ b/packages/kbn-data-stream-adapter/src/data_stream_adapter.ts @@ -7,145 +7,22 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { - ClusterPutComponentTemplateRequest, - IndicesIndexSettings, - IndicesPutIndexTemplateIndexTemplateMapping, - IndicesPutIndexTemplateRequest, -} from '@elastic/elasticsearch/lib/api/types'; -import type { Logger, ElasticsearchClient } from '@kbn/core/server'; -import type { Subject } from 'rxjs'; -import type { FieldMap } from './field_maps/types'; -import { createOrUpdateComponentTemplate } from './create_or_update_component_template'; +import { IndexAdapter, SetIndexTemplateParams, type InstallParams } from '@kbn/index-adapter'; import { createOrUpdateDataStream } from './create_or_update_data_stream'; -import { createOrUpdateIndexTemplate } from './create_or_update_index_template'; -import { InstallShutdownError, installWithTimeout } from './install_with_timeout'; -import { getComponentTemplate, getIndexTemplate } from './resource_installer_utils'; - -export interface DataStreamAdapterParams { - kibanaVersion: string; - totalFieldsLimit?: number; -} -export interface SetComponentTemplateParams { - name: string; - fieldMap: FieldMap; - settings?: IndicesIndexSettings; - dynamic?: 'strict' | boolean; -} -export interface SetIndexTemplateParams { - name: string; - componentTemplateRefs?: string[]; - namespace?: string; - template?: IndicesPutIndexTemplateIndexTemplateMapping; - hidden?: boolean; -} - -export interface GetInstallFnParams { - logger: Logger; - pluginStop$: Subject; - tasksTimeoutMs?: number; -} -export interface InstallParams { - logger: Logger; - esClient: ElasticsearchClient | Promise; - pluginStop$: Subject; - tasksTimeoutMs?: number; -} - -const DEFAULT_FIELDS_LIMIT = 2500; - -export class DataStreamAdapter { - protected readonly kibanaVersion: string; - protected readonly totalFieldsLimit: number; - protected componentTemplates: ClusterPutComponentTemplateRequest[] = []; - protected indexTemplates: IndicesPutIndexTemplateRequest[] = []; - protected installed: boolean; - - constructor(protected readonly name: string, options: DataStreamAdapterParams) { - this.installed = false; - this.kibanaVersion = options.kibanaVersion; - this.totalFieldsLimit = options.totalFieldsLimit ?? DEFAULT_FIELDS_LIMIT; - } - - public setComponentTemplate(params: SetComponentTemplateParams) { - if (this.installed) { - throw new Error('Cannot set component template after install'); - } - this.componentTemplates.push(getComponentTemplate(params)); - } +export class DataStreamAdapter extends IndexAdapter { public setIndexTemplate(params: SetIndexTemplateParams) { - if (this.installed) { - throw new Error('Cannot set index template after install'); - } - this.indexTemplates.push( - getIndexTemplate({ - ...params, - indexPatterns: [this.name], - kibanaVersion: this.kibanaVersion, - totalFieldsLimit: this.totalFieldsLimit, - }) - ); - } - - protected getInstallFn({ logger, pluginStop$, tasksTimeoutMs }: GetInstallFnParams) { - return async (promise: Promise, description?: string): Promise => { - try { - await installWithTimeout({ - installFn: () => promise, - description, - timeoutMs: tasksTimeoutMs, - pluginStop$, - }); - } catch (err) { - if (err instanceof InstallShutdownError) { - logger.info(err.message); - } else { - throw err; - } - } - }; + super.setIndexTemplate({ ...params, isDataStream: true }); } - public async install({ - logger, - esClient: esClientToResolve, - pluginStop$, - tasksTimeoutMs, - }: InstallParams) { + public async install(params: InstallParams) { this.installed = true; + const { logger, pluginStop$, tasksTimeoutMs } = params; + const esClient = await params.esClient; - const esClient = await esClientToResolve; - const installFn = this.getInstallFn({ logger, pluginStop$, tasksTimeoutMs }); - - // Install component templates in parallel - await Promise.all( - this.componentTemplates.map((componentTemplate) => - installFn( - createOrUpdateComponentTemplate({ - template: componentTemplate, - esClient, - logger, - totalFieldsLimit: this.totalFieldsLimit, - }), - `${componentTemplate.name} component template` - ) - ) - ); + await this.installTemplates(params); - // Install index templates in parallel - await Promise.all( - this.indexTemplates.map((indexTemplate) => - installFn( - createOrUpdateIndexTemplate({ - template: indexTemplate, - esClient, - logger, - }), - `${indexTemplate.name} index template` - ) - ) - ); + const installFn = this.getInstallFn({ logger, pluginStop$, tasksTimeoutMs }); // create data stream when everything is ready await installFn( diff --git a/packages/kbn-data-stream-adapter/src/data_stream_spaces_adapter.ts b/packages/kbn-data-stream-adapter/src/data_stream_spaces_adapter.ts index 9ea3c1a4a311f..df131920b7bf9 100644 --- a/packages/kbn-data-stream-adapter/src/data_stream_spaces_adapter.ts +++ b/packages/kbn-data-stream-adapter/src/data_stream_spaces_adapter.ts @@ -7,59 +7,26 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { createOrUpdateComponentTemplate } from './create_or_update_component_template'; -import { createDataStream, updateDataStreams } from './create_or_update_data_stream'; -import { createOrUpdateIndexTemplate } from './create_or_update_index_template'; import { - DataStreamAdapter, - type DataStreamAdapterParams, + IndexPatternAdapter, + type SetIndexTemplateParams, type InstallParams, -} from './data_stream_adapter'; - -export class DataStreamSpacesAdapter extends DataStreamAdapter { - private installedSpaceDataStreamName: Map>; - private _installSpace?: (spaceId: string) => Promise; + type InstallIndex, +} from '@kbn/index-adapter'; +import { createDataStream, updateDataStreams } from './create_or_update_data_stream'; - constructor(private readonly prefix: string, options: DataStreamAdapterParams) { - super(`${prefix}-*`, options); // make indexTemplate `indexPatterns` match all data stream space names - this.installedSpaceDataStreamName = new Map(); +export class DataStreamSpacesAdapter extends IndexPatternAdapter { + public setIndexTemplate(params: SetIndexTemplateParams) { + super.setIndexTemplate({ ...params, isDataStream: true }); } - public async install({ - logger, - esClient: esClientToResolve, - pluginStop$, - tasksTimeoutMs, - }: InstallParams) { - this.installed = true; + protected async _install(params: InstallParams): Promise { + const { logger, pluginStop$, tasksTimeoutMs } = params; - const esClient = await esClientToResolve; - const installFn = this.getInstallFn({ logger, pluginStop$, tasksTimeoutMs }); + await this.installTemplates(params); - // Install component templates in parallel - await Promise.all( - this.componentTemplates.map((componentTemplate) => - installFn( - createOrUpdateComponentTemplate({ - template: componentTemplate, - esClient, - logger, - totalFieldsLimit: this.totalFieldsLimit, - }), - `create or update ${componentTemplate.name} component template` - ) - ) - ); - - // Install index templates in parallel - await Promise.all( - this.indexTemplates.map((indexTemplate) => - installFn( - createOrUpdateIndexTemplate({ template: indexTemplate, esClient, logger }), - `create or update ${indexTemplate.name} index template` - ) - ) - ); + const esClient = await params.esClient; + const installFn = this.getInstallFn({ logger, pluginStop$, tasksTimeoutMs }); // Update existing space data streams await installFn( @@ -72,31 +39,21 @@ export class DataStreamSpacesAdapter extends DataStreamAdapter { `update space data streams` ); - // define function to install data stream for spaces on demand - this._installSpace = async (spaceId: string) => { - const existingInstallPromise = this.installedSpaceDataStreamName.get(spaceId); - if (existingInstallPromise) { - return existingInstallPromise; - } - const name = `${this.prefix}-${spaceId}`; - const installPromise = installFn( - createDataStream({ name, esClient, logger }), - `create ${name} data stream` - ).then(() => name); - - this.installedSpaceDataStreamName.set(spaceId, installPromise); - return installPromise; - }; + // define function to install data stream on demand + return async (name: string) => + installFn(createDataStream({ name, esClient, logger }), `create ${name} data stream`); } + /** + * Method to create the data stream for a given space ID. + * It resolves with the full data stream name. + */ public async installSpace(spaceId: string): Promise { - if (!this._installSpace) { - throw new Error('Cannot installSpace before install'); - } - return this._installSpace(spaceId); + await this.createIndex(spaceId); + return this.getIndexName(spaceId); } public async getInstalledSpaceName(spaceId: string): Promise { - return this.installedSpaceDataStreamName.get(spaceId); + return this.getInstalledIndexName(spaceId); } } diff --git a/packages/kbn-data-stream-adapter/tsconfig.json b/packages/kbn-data-stream-adapter/tsconfig.json index 7eded8e71bef4..8c8bcce97fe74 100644 --- a/packages/kbn-data-stream-adapter/tsconfig.json +++ b/packages/kbn-data-stream-adapter/tsconfig.json @@ -5,18 +5,14 @@ "types": [ "jest", "node", - "react", - "@emotion/react/types/css-prop", - "@testing-library/jest-dom", - "@testing-library/react" ] }, - "include": ["**/*.ts", "**/*.tsx"], + "include": ["**/*.ts"], "kbn_references": [ "@kbn/core", - "@kbn/std", - "@kbn/safer-lodash-set", - "@kbn/logging-mocks", + "@kbn/index-adapter", + ], + "exclude": [ + "target/**/*" ], - "exclude": ["target/**/*"] } diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index 9519e84624cbe..0956816c59ed7 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -33,6 +33,7 @@ export { isESQLColumnGroupable, isESQLFieldGroupable, TextBasedLanguages, + queryCannotBeSampled, } from './src'; export { ENABLE_ESQL, FEEDBACK_LINK } from './constants'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index 6cf2dd031bf07..d56a56c62d6ba 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -22,6 +22,7 @@ export { retrieveMetadataColumns, getQueryColumnsFromESQLQuery, } from './utils/query_parsing_helpers'; +export { queryCannotBeSampled } from './utils/query_cannot_be_sampled'; export { appendToESQLQuery, appendWhereClauseToESQLQuery } from './utils/append_to_query'; export { getESQLQueryColumns, diff --git a/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.test.ts b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.test.ts new file mode 100644 index 0000000000000..e0ff5d01be411 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 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 { queryCannotBeSampled } from './query_cannot_be_sampled'; +describe('queryCannotBeSampled', () => { + it('should return true if query contains "match" function', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | where match(field, "value")' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where match()' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where MATCH()' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where MATCH(fieldName,)' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where MATCH(,)' })).toBe(true); + }); + + it('should return true if query contains "qstr" function', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | where qstr(field, "value")' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where qstr()' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where QSTR()' })).toBe(true); + }); + + it('should return false if query contains names', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | eval match =' })).toBe(false); + expect(queryCannotBeSampled({ esql: 'FROM index | eval MATCH =' })).toBe(false); + expect(queryCannotBeSampled({ esql: 'FROM index | eval qstr =' })).toBe(false); + }); + + it('should return false if query does not contain unsamplable functions', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | eval otherFunction(field, "value")' })).toBe( + false + ); + expect(queryCannotBeSampled({ esql: 'FROM index | where otherFunction(field, "value")' })).toBe( + false + ); + }); + + it('should return false if query is undefined', () => { + expect(queryCannotBeSampled(undefined)).toBe(false); + expect(queryCannotBeSampled(null)).toBe(false); + }); +}); diff --git a/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.ts b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.ts new file mode 100644 index 0000000000000..c4cbd34e75371 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.ts @@ -0,0 +1,46 @@ +/* + * 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 type { AggregateQuery, Query } from '@kbn/es-query'; +import { Walker } from '@kbn/esql-ast'; +import { parse } from '@kbn/esql-ast'; +import { isOfAggregateQueryType } from '@kbn/es-query'; + +/** + * Check if the query contains any of the function names being passed in + * @param query + * @param functions list of function names to check for + * @returns + */ +export const queryContainsFunction = ( + query: AggregateQuery | Query | { [key: string]: any } | undefined | null, + functions: string[] +): boolean => { + if (query && isOfAggregateQueryType(query)) { + const { root } = parse(query.esql); + return functions.some( + (f) => + Walker.hasFunction(root, f) || + // Walker API expects valid queries so we need to do additional check for partial matches + root.commands.some((c) => c.text.toLowerCase().includes(`${f}(`)) + ); + } + return false; +}; + +const UNSAMPLABLE_FUNCTIONS = ['match', 'qstr']; +/** + * Check if the query contains any function that cannot be used after LIMIT clause + * @param query + * @returns + */ +export const queryCannotBeSampled = ( + query: AggregateQuery | Query | { [key: string]: any } | undefined | null +): boolean => { + return queryContainsFunction(query, UNSAMPLABLE_FUNCTIONS); +}; diff --git a/packages/kbn-index-adapter/README.md b/packages/kbn-index-adapter/README.md new file mode 100644 index 0000000000000..e3eb455c2e2cc --- /dev/null +++ b/packages/kbn-index-adapter/README.md @@ -0,0 +1,59 @@ +# @kbn/index-adapter + +Utility library for Elasticsearch index management. + +## IndexAdapter + +Manage single index. Example: + +``` +// Setup +const indexAdapter = new IndexAdapter('my-awesome-index', { kibanaVersion: '8.12.1' }); + +indexAdapter.setComponentTemplate({ + name: 'awesome-component-template', + fieldMap: { + 'awesome.field1: { type: 'keyword', required: true }, + 'awesome.nested.field2: { type: 'number', required: false }, + // ... + }, +}); + +indexAdapter.setIndexTemplate({ + name: 'awesome-index-template', + componentTemplateRefs: ['awesome-component-template', 'ecs-component-template'], +}); + +// Start +await indexAdapter.install({ logger, esClient, pluginStop$ }); // Installs templates and the 'my-awesome-index' index, or updates existing. +``` + + +## IndexPatternAdapter + +Manage index patterns. Example: + +``` +// Setup +const indexPatternAdapter = new IndexPatternAdapter('my-awesome-index', { kibanaVersion: '8.12.1' }); + +indexPatternAdapter.setComponentTemplate({ + name: 'awesome-component-template', + fieldMap: { + 'awesome.field1: { type: 'keyword', required: true }, + 'awesome.nested.field2: { type: 'number', required: false }, + // ... + }, +}); + +indexPatternAdapter.setIndexTemplate({ + name: 'awesome-index-template', + componentTemplateRefs: ['awesome-component-template', 'ecs-component-template'], +}); + +// Start +indexPatternAdapter.install({ logger, esClient, pluginStop$ }); // Installs/updates templates for the index pattern 'my-awesome-index-*', and updates mappings of all specific indices + +// Create a specific index on the fly +await indexPatternAdapter.installIndex('12345'); // creates 'my-awesome-index-12345' index if it does not exist. +``` diff --git a/packages/kbn-index-adapter/index.ts b/packages/kbn-index-adapter/index.ts new file mode 100644 index 0000000000000..6956792135282 --- /dev/null +++ b/packages/kbn-index-adapter/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 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 { IndexAdapter } from './src/index_adapter'; +export { IndexPatternAdapter, type InstallIndex } from './src/index_pattern_adapter'; +export { retryTransientEsErrors } from './src/retry_transient_es_errors'; +export { ecsFieldMap, type EcsFieldMap } from './src/field_maps/ecs_field_map'; +export { createOrUpdateIndexTemplate } from './src/create_or_update_index_template'; +export { createOrUpdateComponentTemplate } from './src/create_or_update_component_template'; + +export type { + SetComponentTemplateParams, + SetIndexTemplateParams, + IndexAdapterParams, + InstallParams, +} from './src/index_adapter'; +export * from './src/field_maps/types'; diff --git a/packages/kbn-index-adapter/jest.config.js b/packages/kbn-index-adapter/jest.config.js new file mode 100644 index 0000000000000..bf08ec1526382 --- /dev/null +++ b/packages/kbn-index-adapter/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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". + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-index-adapter'], +}; diff --git a/packages/kbn-index-adapter/kibana.jsonc b/packages/kbn-index-adapter/kibana.jsonc new file mode 100644 index 0000000000000..575d95f5a3e39 --- /dev/null +++ b/packages/kbn-index-adapter/kibana.jsonc @@ -0,0 +1,6 @@ +{ + "type": "shared-server", + "id": "@kbn/index-adapter", + "owner": "@elastic/security-threat-hunting", + "visibility": "shared" +} diff --git a/packages/kbn-index-adapter/package.json b/packages/kbn-index-adapter/package.json new file mode 100644 index 0000000000000..70b79abe1b571 --- /dev/null +++ b/packages/kbn-index-adapter/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/index-adapter", + "version": "1.0.0", + "description": "Utility library for Elasticsearch index management", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", + "private": true +} \ No newline at end of file diff --git a/packages/kbn-data-stream-adapter/src/create_or_update_component_template.test.ts b/packages/kbn-index-adapter/src/create_or_update_component_template.test.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/create_or_update_component_template.test.ts rename to packages/kbn-index-adapter/src/create_or_update_component_template.test.ts diff --git a/packages/kbn-data-stream-adapter/src/create_or_update_component_template.ts b/packages/kbn-index-adapter/src/create_or_update_component_template.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/create_or_update_component_template.ts rename to packages/kbn-index-adapter/src/create_or_update_component_template.ts diff --git a/packages/kbn-index-adapter/src/create_or_update_index.test.ts b/packages/kbn-index-adapter/src/create_or_update_index.test.ts new file mode 100644 index 0000000000000..6c32b183e1fda --- /dev/null +++ b/packages/kbn-index-adapter/src/create_or_update_index.test.ts @@ -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", 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 { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { updateIndices, createIndex, createOrUpdateIndex } from './create_or_update_index'; + +const logger = loggingSystemMock.createLogger(); +const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + +esClient.indices.putMapping.mockResolvedValue({ acknowledged: true }); +esClient.indices.putSettings.mockResolvedValue({ acknowledged: true }); + +const simulateIndexTemplateResponse = { template: { mappings: {}, settings: {}, aliases: {} } }; +esClient.indices.simulateIndexTemplate.mockResolvedValue(simulateIndexTemplateResponse); + +const name = 'test_index_name'; +const totalFieldsLimit = 1000; + +describe('updateIndices', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it(`should update indices`, async () => { + const indexName = 'test_index_name-default'; + esClient.indices.get.mockResolvedValueOnce({ [indexName]: {} }); + + await updateIndices({ + esClient, + logger, + name, + totalFieldsLimit, + }); + + expect(esClient.indices.get).toHaveBeenCalledWith({ + index: name, + expand_wildcards: 'all', + }); + + expect(esClient.indices.putSettings).toHaveBeenCalledWith({ + index: indexName, + body: { 'index.mapping.total_fields.limit': totalFieldsLimit }, + }); + expect(esClient.indices.simulateIndexTemplate).toHaveBeenCalledWith({ + name: indexName, + }); + expect(esClient.indices.putMapping).toHaveBeenCalledWith({ + index: indexName, + body: simulateIndexTemplateResponse.template.mappings, + }); + }); + + it(`should update multiple indices`, async () => { + const indexName1 = 'test_index_name-1'; + const indexName2 = 'test_index_name-2'; + esClient.indices.get.mockResolvedValueOnce({ [indexName1]: {}, [indexName2]: {} }); + + await updateIndices({ + esClient, + logger, + name, + totalFieldsLimit, + }); + + expect(esClient.indices.putSettings).toHaveBeenCalledTimes(2); + expect(esClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(2); + expect(esClient.indices.putMapping).toHaveBeenCalledTimes(2); + }); + + it(`should not update indices when not exist`, async () => { + esClient.indices.get.mockResolvedValueOnce({}); + + await updateIndices({ + esClient, + logger, + name, + totalFieldsLimit, + }); + + expect(esClient.indices.putSettings).not.toHaveBeenCalled(); + expect(esClient.indices.simulateIndexTemplate).not.toHaveBeenCalled(); + expect(esClient.indices.putMapping).not.toHaveBeenCalled(); + }); +}); + +describe('createIndex', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it(`should create index`, async () => { + esClient.indices.exists.mockResolvedValueOnce(false); + + await createIndex({ + esClient, + logger, + name, + }); + + expect(esClient.indices.exists).toHaveBeenCalledWith({ index: name, expand_wildcards: 'all' }); + expect(esClient.indices.create).toHaveBeenCalledWith({ index: name }); + }); + + it(`should not create index if already exists`, async () => { + esClient.indices.exists.mockResolvedValueOnce(true); + + await createIndex({ + esClient, + logger, + name, + }); + + expect(esClient.indices.exists).toHaveBeenCalledWith({ index: name, expand_wildcards: 'all' }); + expect(esClient.indices.create).not.toHaveBeenCalled(); + }); +}); + +describe('createOrUpdateIndex', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it(`should create index if not exists`, async () => { + esClient.indices.exists.mockResolvedValueOnce(false); + + await createOrUpdateIndex({ + esClient, + logger, + name, + totalFieldsLimit, + }); + + expect(esClient.indices.create).toHaveBeenCalledWith({ index: name }); + }); + + it(`should update index if already exists`, async () => { + esClient.indices.exists.mockResolvedValueOnce(true); + + await createOrUpdateIndex({ + esClient, + logger, + name, + totalFieldsLimit, + }); + + expect(esClient.indices.exists).toHaveBeenCalledWith({ index: name, expand_wildcards: 'all' }); + + expect(esClient.indices.putSettings).toHaveBeenCalledWith({ + index: name, + body: { 'index.mapping.total_fields.limit': totalFieldsLimit }, + }); + expect(esClient.indices.simulateIndexTemplate).toHaveBeenCalledWith({ + name, + }); + expect(esClient.indices.putMapping).toHaveBeenCalledWith({ + index: name, + body: simulateIndexTemplateResponse.template.mappings, + }); + }); +}); diff --git a/packages/kbn-index-adapter/src/create_or_update_index.ts b/packages/kbn-index-adapter/src/create_or_update_index.ts new file mode 100644 index 0000000000000..ff825c61305b7 --- /dev/null +++ b/packages/kbn-index-adapter/src/create_or_update_index.ts @@ -0,0 +1,237 @@ +/* + * 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 type { IndexName } from '@elastic/elasticsearch/lib/api/types'; +import type { IndicesSimulateIndexTemplateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import { get } from 'lodash'; +import { retryTransientEsErrors } from './retry_transient_es_errors'; + +interface UpdateIndexMappingsOpts { + logger: Logger; + esClient: ElasticsearchClient; + indexNames: string[]; + totalFieldsLimit: number; +} + +interface UpdateIndexOpts { + logger: Logger; + esClient: ElasticsearchClient; + indexName: string; + totalFieldsLimit: number; +} + +const updateTotalFieldLimitSetting = async ({ + logger, + esClient, + indexName, + totalFieldsLimit, +}: UpdateIndexOpts) => { + logger.debug(`Updating total field limit setting for ${indexName} data stream.`); + + try { + const body = { 'index.mapping.total_fields.limit': totalFieldsLimit }; + await retryTransientEsErrors(() => esClient.indices.putSettings({ index: indexName, body }), { + logger, + }); + } catch (err) { + logger.error( + `Failed to PUT index.mapping.total_fields.limit settings for ${indexName}: ${err.message}` + ); + throw err; + } +}; + +// This will update the mappings but *not* the settings. This +// is due to the fact settings can be classed as dynamic and static, and static +// updates will fail on an index that isn't closed. New settings *will* be applied as part +// of the ILM policy rollovers. More info: https://github.com/elastic/kibana/pull/113389#issuecomment-940152654 +const updateMapping = async ({ logger, esClient, indexName }: UpdateIndexOpts) => { + logger.debug(`Updating mappings for ${indexName} data stream.`); + + let simulatedIndexMapping: IndicesSimulateIndexTemplateResponse; + try { + simulatedIndexMapping = await retryTransientEsErrors( + () => esClient.indices.simulateIndexTemplate({ name: indexName }), + { logger } + ); + } catch (err) { + logger.error( + `Ignored PUT mappings for ${indexName}; error generating simulated mappings: ${err.message}` + ); + return; + } + + const simulatedMapping = get(simulatedIndexMapping, ['template', 'mappings']); + + if (simulatedMapping == null) { + logger.error(`Ignored PUT mappings for ${indexName}; simulated mappings were empty`); + return; + } + + try { + await retryTransientEsErrors( + () => esClient.indices.putMapping({ index: indexName, body: simulatedMapping }), + { logger } + ); + } catch (err) { + logger.error(`Failed to PUT mapping for ${indexName}: ${err.message}`); + throw err; + } +}; +/** + * Updates the data stream mapping and total field limit setting + */ +const updateIndexMappings = async ({ + logger, + esClient, + totalFieldsLimit, + indexNames, +}: UpdateIndexMappingsOpts) => { + // Update total field limit setting of found indices + // Other index setting changes are not updated at this time + await Promise.all( + indexNames.map((indexName) => + updateTotalFieldLimitSetting({ logger, esClient, totalFieldsLimit, indexName }) + ) + ); + // Update mappings of the found indices. + await Promise.all( + indexNames.map((indexName) => updateMapping({ logger, esClient, totalFieldsLimit, indexName })) + ); +}; + +export interface CreateOrUpdateIndexParams { + name: string; + logger: Logger; + esClient: ElasticsearchClient; + totalFieldsLimit: number; +} + +export async function createOrUpdateIndex({ + logger, + esClient, + name, + totalFieldsLimit, +}: CreateOrUpdateIndexParams): Promise { + logger.info(`Creating index - ${name}`); + + // check if index exists + let indexExists = false; + try { + indexExists = await retryTransientEsErrors( + () => esClient.indices.exists({ index: name, expand_wildcards: 'all' }), + { logger } + ); + } catch (error) { + if (error?.statusCode !== 404) { + logger.error(`Error fetching index for ${name} - ${error.message}`); + throw error; + } + } + + // if a index exists, update the underlying mapping + if (indexExists) { + await updateIndexMappings({ + logger, + esClient, + indexNames: [name], + totalFieldsLimit, + }); + } else { + try { + await retryTransientEsErrors(() => esClient.indices.create({ index: name }), { logger }); + } catch (error) { + if (error?.meta?.body?.error?.type !== 'resource_already_exists_exception') { + logger.error(`Error creating index ${name} - ${error.message}`); + throw error; + } + } + } +} + +export interface CreateIndexParams { + name: string; + logger: Logger; + esClient: ElasticsearchClient; +} + +export async function createIndex({ logger, esClient, name }: CreateIndexParams): Promise { + logger.debug(`Checking existence of index - ${name}`); + + // check if index exists + let indexExists = false; + try { + indexExists = await retryTransientEsErrors( + () => esClient.indices.exists({ index: name, expand_wildcards: 'all' }), + { + logger, + } + ); + } catch (error) { + if (error?.statusCode !== 404) { + logger.error(`Error fetching index for ${name} - ${error.message}`); + throw error; + } + } + + // return if index already created + if (indexExists) { + return; + } + + logger.info(`Creating index - ${name}`); + try { + await retryTransientEsErrors(() => esClient.indices.create({ index: name }), { logger }); + } catch (error) { + if (error?.meta?.body?.error?.type !== 'resource_already_exists_exception') { + logger.error(`Error creating index ${name} - ${error.message}`); + throw error; + } + } +} + +export interface CreateOrUpdateSpacesIndexParams { + name: string; + logger: Logger; + esClient: ElasticsearchClient; + totalFieldsLimit: number; +} + +export async function updateIndices({ + logger, + esClient, + name, + totalFieldsLimit, +}: CreateOrUpdateSpacesIndexParams): Promise { + logger.info(`Updating indices - ${name}`); + + // check if data stream exists + let indices: IndexName[] = []; + try { + const response = await retryTransientEsErrors( + () => esClient.indices.get({ index: name, expand_wildcards: 'all' }), + { logger } + ); + indices = Object.keys(response); + } catch (error) { + if (error?.statusCode !== 404) { + logger.error(`Error fetching indices for ${name} - ${error.message}`); + throw error; + } + } + if (indices.length > 0) { + await updateIndexMappings({ + logger, + esClient, + totalFieldsLimit, + indexNames: indices, + }); + } +} diff --git a/packages/kbn-data-stream-adapter/src/create_or_update_index_template.test.ts b/packages/kbn-index-adapter/src/create_or_update_index_template.test.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/create_or_update_index_template.test.ts rename to packages/kbn-index-adapter/src/create_or_update_index_template.test.ts diff --git a/packages/kbn-data-stream-adapter/src/create_or_update_index_template.ts b/packages/kbn-index-adapter/src/create_or_update_index_template.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/create_or_update_index_template.ts rename to packages/kbn-index-adapter/src/create_or_update_index_template.ts diff --git a/packages/kbn-data-stream-adapter/src/field_maps/ecs_field_map.ts b/packages/kbn-index-adapter/src/field_maps/ecs_field_map.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/field_maps/ecs_field_map.ts rename to packages/kbn-index-adapter/src/field_maps/ecs_field_map.ts diff --git a/packages/kbn-data-stream-adapter/src/field_maps/mapping_from_field_map.test.ts b/packages/kbn-index-adapter/src/field_maps/mapping_from_field_map.test.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/field_maps/mapping_from_field_map.test.ts rename to packages/kbn-index-adapter/src/field_maps/mapping_from_field_map.test.ts diff --git a/packages/kbn-data-stream-adapter/src/field_maps/mapping_from_field_map.ts b/packages/kbn-index-adapter/src/field_maps/mapping_from_field_map.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/field_maps/mapping_from_field_map.ts rename to packages/kbn-index-adapter/src/field_maps/mapping_from_field_map.ts diff --git a/packages/kbn-data-stream-adapter/src/field_maps/types.ts b/packages/kbn-index-adapter/src/field_maps/types.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/field_maps/types.ts rename to packages/kbn-index-adapter/src/field_maps/types.ts diff --git a/packages/kbn-index-adapter/src/index_adapter.ts b/packages/kbn-index-adapter/src/index_adapter.ts new file mode 100644 index 0000000000000..eef2ce529d78a --- /dev/null +++ b/packages/kbn-index-adapter/src/index_adapter.ts @@ -0,0 +1,158 @@ +/* + * 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 type { + ClusterPutComponentTemplateRequest, + IndicesPutIndexTemplateRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { Subject } from 'rxjs'; +import { createOrUpdateComponentTemplate } from './create_or_update_component_template'; +import { createOrUpdateIndex } from './create_or_update_index'; +import { createOrUpdateIndexTemplate } from './create_or_update_index_template'; +import { InstallShutdownError, installWithTimeout } from './install_with_timeout'; +import { + getComponentTemplate, + getIndexTemplate, + type GetComponentTemplateOpts, + type GetIndexTemplateOpts, +} from './resource_installer_utils'; + +export interface IndexAdapterParams { + kibanaVersion: string; + totalFieldsLimit?: number; +} +export type SetComponentTemplateParams = GetComponentTemplateOpts; +export type SetIndexTemplateParams = Omit< + GetIndexTemplateOpts, + 'indexPatterns' | 'kibanaVersion' | 'totalFieldsLimit' +>; +export interface GetInstallFnParams { + logger: Logger; + pluginStop$: Subject; + tasksTimeoutMs?: number; +} +export interface InstallParams { + logger: Logger; + esClient: ElasticsearchClient | Promise; + pluginStop$: Subject; + tasksTimeoutMs?: number; +} + +const DEFAULT_FIELDS_LIMIT = 2500; + +export class IndexAdapter { + protected readonly kibanaVersion: string; + protected readonly totalFieldsLimit: number; + protected componentTemplates: ClusterPutComponentTemplateRequest[] = []; + protected indexTemplates: IndicesPutIndexTemplateRequest[] = []; + protected installed: boolean; + + constructor(protected readonly name: string, options: IndexAdapterParams) { + this.installed = false; + this.kibanaVersion = options.kibanaVersion; + this.totalFieldsLimit = options.totalFieldsLimit ?? DEFAULT_FIELDS_LIMIT; + } + + public setComponentTemplate(params: SetComponentTemplateParams) { + if (this.installed) { + throw new Error('Cannot set component template after install'); + } + this.componentTemplates.push(getComponentTemplate(params)); + } + + public setIndexTemplate(params: SetIndexTemplateParams) { + if (this.installed) { + throw new Error('Cannot set index template after install'); + } + this.indexTemplates.push( + getIndexTemplate({ + ...params, + indexPatterns: [this.name], + kibanaVersion: this.kibanaVersion, + totalFieldsLimit: this.totalFieldsLimit, + }) + ); + } + + protected getInstallFn({ logger, pluginStop$, tasksTimeoutMs }: GetInstallFnParams) { + return async (promise: Promise, description?: string): Promise => { + try { + await installWithTimeout({ + installFn: () => promise, + description, + timeoutMs: tasksTimeoutMs, + pluginStop$, + }); + } catch (err) { + if (err instanceof InstallShutdownError) { + logger.info(err.message); + } else { + throw err; + } + } + }; + } + + protected async installTemplates(params: InstallParams) { + const { logger, pluginStop$, tasksTimeoutMs } = params; + const esClient = await params.esClient; + const installFn = this.getInstallFn({ logger, pluginStop$, tasksTimeoutMs }); + + // Install component templates in parallel + await Promise.all( + this.componentTemplates.map((componentTemplate) => + installFn( + createOrUpdateComponentTemplate({ + template: componentTemplate, + esClient, + logger, + totalFieldsLimit: this.totalFieldsLimit, + }), + `create or update ${componentTemplate.name} component template` + ) + ) + ); + + // Install index templates in parallel + await Promise.all( + this.indexTemplates.map((indexTemplate) => + installFn( + createOrUpdateIndexTemplate({ + template: indexTemplate, + esClient, + logger, + }), + `create or update ${indexTemplate.name} index template` + ) + ) + ); + } + + public async install(params: InstallParams) { + this.installed = true; + const { logger, pluginStop$, tasksTimeoutMs } = params; + const esClient = await params.esClient; + + await this.installTemplates(params); + + const installFn = this.getInstallFn({ logger, pluginStop$, tasksTimeoutMs }); + + // create index when everything is ready + await installFn( + createOrUpdateIndex({ + name: this.name, + esClient, + logger, + totalFieldsLimit: this.totalFieldsLimit, + }), + `${this.name} index` + ); + } +} diff --git a/packages/kbn-index-adapter/src/index_pattern_adapter.ts b/packages/kbn-index-adapter/src/index_pattern_adapter.ts new file mode 100644 index 0000000000000..38a96a3c65b83 --- /dev/null +++ b/packages/kbn-index-adapter/src/index_pattern_adapter.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 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 { createIndex, updateIndices } from './create_or_update_index'; +import { IndexAdapter, type IndexAdapterParams, type InstallParams } from './index_adapter'; + +export type InstallIndex = (indexSuffix: string) => Promise; + +export class IndexPatternAdapter extends IndexAdapter { + protected installationPromises: Map>; + protected installIndexPromise?: Promise; + + constructor(protected readonly prefix: string, options: IndexAdapterParams) { + super(`${prefix}-*`, options); // make indexTemplate `indexPatterns` match all index names + this.installationPromises = new Map(); + } + + /** Method to create/update the templates, update existing indices and setup internal state for the adapter. */ + public async install(params: InstallParams): Promise { + this.installIndexPromise = this._install(params); + await this.installIndexPromise; + } + + protected async _install(params: InstallParams): Promise { + const { logger, pluginStop$, tasksTimeoutMs } = params; + + await this.installTemplates(params); + + const esClient = await params.esClient; + const installFn = this.getInstallFn({ logger, pluginStop$, tasksTimeoutMs }); + + // Update existing specific indices + await installFn( + updateIndices({ + name: this.name, // `${prefix}-*` + esClient, + logger, + totalFieldsLimit: this.totalFieldsLimit, + }), + `update specific indices` + ); + + // Define the function to create concrete indices on demand + return async (name: string) => + installFn(createIndex({ name, esClient, logger }), `create ${name} index`); + } + + /** + * Method to create the index for a given index suffix. + * Stores the installations promises to avoid concurrent installations for the same index. + * Index creation will only be attempted once per index suffix and existence will be checked before creating. + */ + public async createIndex(indexSuffix: string): Promise { + if (!this.installIndexPromise) { + throw new Error('Cannot installIndex before install'); + } + + const existingInstallation = this.installationPromises.get(indexSuffix); + if (existingInstallation) { + return existingInstallation; + } + const indexName = this.getIndexName(indexSuffix); + + // Awaits for installIndexPromise to resolve to ensure templates are installed before the specific index is created. + // This is a safety measure since the initial `install` call may not be awaited from the plugin lifecycle caller. + // However, the promise will most likely be already fulfilled by the time `createIndex` is called, so this is a no-op. + const installation = this.installIndexPromise + .then((installIndex) => installIndex(indexName)) + .catch((err) => { + this.installationPromises.delete(indexSuffix); + throw err; + }); + + this.installationPromises.set(indexSuffix, installation); + return installation; + } + + /** Method to get the full index name for a given index suffix. */ + public getIndexName(indexSuffix: string): string { + return `${this.prefix}-${indexSuffix}`; + } + + /** Method to get the full index name for a given index suffix. It returns undefined if the index does not exist. */ + public async getInstalledIndexName(indexSuffix: string): Promise { + const existingInstallation = this.installationPromises.get(indexSuffix); + if (!existingInstallation) { + return undefined; + } + return existingInstallation.then(() => this.getIndexName(indexSuffix)).catch(() => undefined); + } +} diff --git a/packages/kbn-data-stream-adapter/src/install_with_timeout.test.ts b/packages/kbn-index-adapter/src/install_with_timeout.test.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/install_with_timeout.test.ts rename to packages/kbn-index-adapter/src/install_with_timeout.test.ts diff --git a/packages/kbn-data-stream-adapter/src/install_with_timeout.ts b/packages/kbn-index-adapter/src/install_with_timeout.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/install_with_timeout.ts rename to packages/kbn-index-adapter/src/install_with_timeout.ts diff --git a/packages/kbn-data-stream-adapter/src/resource_installer_utils.test.ts b/packages/kbn-index-adapter/src/resource_installer_utils.test.ts similarity index 92% rename from packages/kbn-data-stream-adapter/src/resource_installer_utils.test.ts rename to packages/kbn-index-adapter/src/resource_installer_utils.test.ts index 93d421bb5605c..31d4a3abcbb0d 100644 --- a/packages/kbn-data-stream-adapter/src/resource_installer_utils.test.ts +++ b/packages/kbn-index-adapter/src/resource_installer_utils.test.ts @@ -24,7 +24,6 @@ describe('getIndexTemplate', () => { expect(indexTemplate).toEqual({ name: defaultParams.name, body: { - data_stream: { hidden: true }, index_patterns: defaultParams.indexPatterns, composed_of: defaultParams.componentTemplateRefs, template: { @@ -57,8 +56,17 @@ describe('getIndexTemplate', () => { }); }); + it('should create data stream index template with given parameters and defaults', () => { + const indexTemplate = getIndexTemplate({ ...defaultParams, isDataStream: true }); + expect(indexTemplate.body).toEqual( + expect.objectContaining({ + data_stream: { hidden: true }, + }) + ); + }); + it('should create not hidden index template', () => { - const { body } = getIndexTemplate({ ...defaultParams, hidden: false }); + const { body } = getIndexTemplate({ ...defaultParams, isDataStream: true, hidden: false }); expect(body?.data_stream?.hidden).toEqual(false); expect(body?.template?.settings?.hidden).toEqual(false); }); diff --git a/packages/kbn-data-stream-adapter/src/resource_installer_utils.ts b/packages/kbn-index-adapter/src/resource_installer_utils.ts similarity index 93% rename from packages/kbn-data-stream-adapter/src/resource_installer_utils.ts rename to packages/kbn-index-adapter/src/resource_installer_utils.ts index 96b220cf0983c..eb6e2490000b2 100644 --- a/packages/kbn-data-stream-adapter/src/resource_installer_utils.ts +++ b/packages/kbn-index-adapter/src/resource_installer_utils.ts @@ -19,7 +19,7 @@ import type { import type { FieldMap } from './field_maps/types'; import { mappingFromFieldMap } from './field_maps/mapping_from_field_map'; -interface GetComponentTemplateOpts { +export interface GetComponentTemplateOpts { name: string; fieldMap: FieldMap; settings?: IndicesIndexSettings; @@ -47,7 +47,7 @@ export const getComponentTemplate = ({ }, }); -interface GetIndexTemplateOpts { +export interface GetIndexTemplateOpts { name: string; indexPatterns: string[]; kibanaVersion: string; @@ -56,6 +56,7 @@ interface GetIndexTemplateOpts { namespace?: string; template?: IndicesPutIndexTemplateIndexTemplateMapping; hidden?: boolean; + isDataStream?: boolean; } export const getIndexTemplate = ({ @@ -67,6 +68,7 @@ export const getIndexTemplate = ({ namespace = 'default', template = {}, hidden = true, + isDataStream = false, }: GetIndexTemplateOpts): IndicesPutIndexTemplateRequest => { const indexMetadata: Metadata = { kibana: { @@ -79,7 +81,7 @@ export const getIndexTemplate = ({ return { name, body: { - data_stream: { hidden }, + ...(isDataStream && { data_stream: { hidden } }), index_patterns: indexPatterns, composed_of: componentTemplateRefs, template: { diff --git a/packages/kbn-data-stream-adapter/src/retry_transient_es_errors.test.ts b/packages/kbn-index-adapter/src/retry_transient_es_errors.test.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/retry_transient_es_errors.test.ts rename to packages/kbn-index-adapter/src/retry_transient_es_errors.test.ts diff --git a/packages/kbn-data-stream-adapter/src/retry_transient_es_errors.ts b/packages/kbn-index-adapter/src/retry_transient_es_errors.ts similarity index 100% rename from packages/kbn-data-stream-adapter/src/retry_transient_es_errors.ts rename to packages/kbn-index-adapter/src/retry_transient_es_errors.ts diff --git a/packages/kbn-index-adapter/tsconfig.json b/packages/kbn-index-adapter/tsconfig.json new file mode 100644 index 0000000000000..cca50adbf7eb8 --- /dev/null +++ b/packages/kbn-index-adapter/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + ] + }, + "include": ["**/*.ts"], + "kbn_references": [ + "@kbn/core", + "@kbn/std", + "@kbn/safer-lodash-set", + "@kbn/logging-mocks", + ], + "exclude": [ + "target/**/*" + ], +} diff --git a/packages/kbn-lint-packages-cli/migrate_plugins_to_package.ts b/packages/kbn-lint-packages-cli/migrate_plugins_to_package.ts index 8641fa3c324d9..789729993c0bc 100644 --- a/packages/kbn-lint-packages-cli/migrate_plugins_to_package.ts +++ b/packages/kbn-lint-packages-cli/migrate_plugins_to_package.ts @@ -78,7 +78,8 @@ export async function migratePluginsToPackages(legacyManifests: RepoPath[]) { .split('\n') .flatMap((line) => { const trim = line.trim(); - if (!trim || trim.startsWith('#')) { + // kibanamachine is an assignment override on backport branches to avoid review requests + if (!trim || trim.startsWith('#') || trim.includes('@kibanamachine')) { return []; } diff --git a/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx b/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx index aab65e08f3a6f..2ed392abf1884 100755 --- a/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx +++ b/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx @@ -12,8 +12,16 @@ import { EuiText } from '@elastic/eui'; export interface FieldSummaryMessageProps { message: string; + dataTestSubj?: string; } -export const FieldSummaryMessage: React.FC = ({ message }) => { - return {message}; +export const FieldSummaryMessage: React.FC = ({ + message, + dataTestSubj, +}) => { + return ( + + {message} + + ); }; diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js index c384d4f010dc3..ed027e184ef75 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/parse_owners.js @@ -13,8 +13,9 @@ import { pipe } from '../utils'; const allLines$ = (lineReader) => fromEvent(lineReader, 'line').pipe( - filter(function dropEmptiesAndDropComments(x) { - return x !== '' && !/^#\s{1,3}/.test(x); + filter(function dropEmptiesAndDropCommentsAndDropKibanamachine(x) { + // kibanamachine is an assignment override on backport branches to avoid review requests + return x !== '' && !/^#\s{1,3}/.test(x) && !x.includes('@kibanamachine'); }), map(pipe(dropCCDelim, pathAndTeams)), takeUntil(fromEvent(lineReader, 'close')) diff --git a/src/plugins/controls/public/controls/timeslider_control/init_time_range_subscription.ts b/src/plugins/controls/public/controls/timeslider_control/init_time_range_subscription.ts index 7934e9deaa9b4..a31b9810c7481 100644 --- a/src/plugins/controls/public/controls/timeslider_control/init_time_range_subscription.ts +++ b/src/plugins/controls/public/controls/timeslider_control/init_time_range_subscription.ts @@ -11,7 +11,8 @@ import { TimeRange } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { apiHasParentApi, apiPublishesTimeRange } from '@kbn/presentation-publishing'; import moment from 'moment'; -import { BehaviorSubject, skip } from 'rxjs'; +import { BehaviorSubject, Subscription, skip } from 'rxjs'; +import { apiPublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload'; import { getTimeRangeMeta, getTimezone, TimeRangeMeta } from './get_time_range_meta'; import { getMomentTimezone } from './time_utils'; @@ -26,6 +27,13 @@ export function initTimeRangeSubscription(controlGroupApi: unknown) { timeRangeMeta$.next(getTimeRangeMeta(timeRange)); }); + let reloadSubscription: undefined | Subscription; + if (apiHasParentApi(controlGroupApi) && apiPublishesReload(controlGroupApi.parentApi)) { + reloadSubscription = controlGroupApi.parentApi.reload$.subscribe(() => { + timeRangeMeta$.next(getTimeRangeMeta(timeRange$.value)); + }); + } + return { timeRangeMeta$, formatDate: (epoch: number) => { @@ -35,6 +43,7 @@ export function initTimeRangeSubscription(controlGroupApi: unknown) { .format(timeRangeMeta$.value.format); }, cleanupTimeRangeSubscription: () => { + reloadSubscription?.unsubscribe(); timeRangeSubscription.unsubscribe(); }, }; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 35137075befe4..e21a2f94bfc51 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -876,7 +876,9 @@ export class DashboardContainer const references = getReferencesForPanelId(childId, this.savedObjectReferences); return { rawState, - references, + // references from old installations may not be prefixed with panel id + // fall back to passing all references in these cases to preserve backwards compatability + references: references.length > 0 ? references : this.savedObjectReferences, }; }; diff --git a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx index c9ac877474e87..5a106b50360bb 100644 --- a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx +++ b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx @@ -9,9 +9,10 @@ import React from 'react'; import { useQuerySubscriber } from '@kbn/unified-field-list/src/hooks/use_query_subscriber'; -import { FieldStatisticsTable, type FieldStatisticsTableProps } from './field_stats_table'; -import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { FieldStatisticsTable, type FieldStatisticsTableProps } from './field_stats_table'; +import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; export const FieldStatisticsTab: React.FC> = React.memo((props) => { @@ -20,8 +21,13 @@ export const FieldStatisticsTab: React.FC; + } - if (!services.dataVisualizer) return null; return ( { expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(false); }); - it('should show document and field stats view if ES|QL', async () => { + it('should not show document and field stats view if ES|QL', async () => { const component = await mountComponent({ isEsqlMode: true }); - expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(false); expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true); - expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(false); expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(false); - expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(true); - expect(findTestSubject(component, 'dscViewModeDocumentButton').text()).toBe('Results (10)'); + expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(false); + expect(findTestSubject(component, 'discoverQueryHits').text()).toBe('10'); }); it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', async () => { diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx index 22c4aaa11b43a..43960f98ea2b8 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx @@ -46,10 +46,16 @@ export const DocumentViewModeToggle = ({ () => isLegacyTableEnabled({ uiSettings, isEsqlMode }), [uiSettings, isEsqlMode] ); + const [showPatternAnalysisTab, setShowPatternAnalysisTab] = useState(null); const showFieldStatisticsTab = useMemo( - () => uiSettings.get(SHOW_FIELD_STATISTICS) && dataVisualizerService !== undefined, - [dataVisualizerService, uiSettings] + () => + // If user opens saved search with field stats in ES|QL, + // we show the toggle with the mode disabled so user can switch to document view + // instead of auto-directing + (viewMode === VIEW_MODE.AGGREGATED_LEVEL && isEsqlMode) || + (!isEsqlMode && uiSettings.get(SHOW_FIELD_STATISTICS) && dataVisualizerService !== undefined), + [dataVisualizerService, uiSettings, isEsqlMode, viewMode] ); const isMounted = useMountedState(); @@ -100,6 +106,12 @@ export const DocumentViewModeToggle = ({ } `; + useEffect(() => { + if (viewMode === VIEW_MODE.AGGREGATED_LEVEL && isEsqlMode) { + setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL); + } + }, [viewMode, isEsqlMode, setDiscoverViewMode]); + return ( setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)} data-test-subj="dscViewModeFieldStatsButton" diff --git a/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx b/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx index ede93bf6b6f72..cb1cd2f9eebc3 100644 --- a/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx +++ b/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx @@ -13,10 +13,10 @@ import { BehaviorSubject } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; import { FetchContext, useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; - import { FieldStatisticsTable } from '../../application/main/components/field_stats_table'; import { isEsqlMode } from '../initialize_fetch'; import type { SearchEmbeddableApi, SearchEmbeddableStateManager } from '../types'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; interface SavedSearchEmbeddableComponentProps { api: SearchEmbeddableApi & { @@ -37,8 +37,13 @@ export function SearchEmbeddablFieldStatsTableComponent({ api.fetchContext$, api.savedSearch$ ); - const isEsql = useMemo(() => isEsqlMode(savedSearch), [savedSearch]); + const services = useDiscoverServices(); + + // Quit early if we know it's in ES|QL mode + if (isEsql && services.dataVisualizer?.FieldStatsUnavailableMessage) { + return ; + } return ( { before(async () => { await visualize.initTests(); + await timePicker.resetDefaultAbsoluteRangeViaUiSettings(); await common.navigateToApp('visualize'); await visualize.loadSavedVisualization('input control options', { navigateToVisualize: false, diff --git a/test/functional/apps/discover/esql/_esql_view.ts b/test/functional/apps/discover/esql/_esql_view.ts index 18841d07dac3d..c5599c409d7b8 100644 --- a/test/functional/apps/discover/esql/_esql_view.ts +++ b/test/functional/apps/discover/esql/_esql_view.ts @@ -91,7 +91,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('showQueryBarMenu')).to.be(false); expect(await testSubjects.exists('addFilter')).to.be(false); - expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(true); + expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(false); // when Lens suggests a table, we render an ESQL based histogram expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); expect(await testSubjects.exists('discoverQueryHits')).to.be(true); diff --git a/test/functional/apps/discover/group6/_field_stats_table.ts b/test/functional/apps/discover/group6/_field_stats_table.ts index c8381976ffbab..4d295ef5ca95c 100644 --- a/test/functional/apps/discover/group6/_field_stats_table.ts +++ b/test/functional/apps/discover/group6/_field_stats_table.ts @@ -64,13 +64,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('discoverDocTable'); }); - it('should show Field Statistics data in ES|QL mode', async () => { + it('should not show Field Statistics data in ES|QL mode', async () => { await discover.selectTextBaseLang(); - await discover.waitUntilSearchingHasFinished(); - - await testSubjects.click('dscViewModeFieldStatsButton'); await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('dataVisualizerTableContainer'); + await discover.waitUntilSearchingHasFinished(); + await testSubjects.missingOrFail('dscViewModeFieldStatsButton'); }); }); }); diff --git a/test/functional/apps/discover/group6/_view_mode_toggle.ts b/test/functional/apps/discover/group6/_view_mode_toggle.ts index aec1cb6f459ab..2591716c87d00 100644 --- a/test/functional/apps/discover/group6/_view_mode_toggle.ts +++ b/test/functional/apps/discover/group6/_view_mode_toggle.ts @@ -95,25 +95,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should not show', async function () { await testSubjects.missingOrFail('dscViewModePatternAnalysisButton'); - await retry.try(async () => { - const documentTab = await testSubjects.find('dscViewModeDocumentButton'); - expect(await documentTab.getAttribute('aria-selected')).to.be('true'); - }); }); }); - it('should show Field Statistics tab', async () => { - await testSubjects.click('dscViewModeFieldStatsButton'); - - await retry.try(async () => { - const fieldStatsTab = await testSubjects.find('dscViewModeFieldStatsButton'); - expect(await fieldStatsTab.getAttribute('aria-selected')).to.be('true'); - }); - + it('should not show Field Statistics tab', async () => { await testSubjects.existOrFail('dscViewModeToggle'); }); - it('should still show view mode toggle for ES|QL searches', async () => { + it('should not show view mode toggle for ES|QL searches', async () => { await testSubjects.click('dscViewModeDocumentButton'); await retry.try(async () => { @@ -125,7 +114,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.selectTextBaseLang(); - await testSubjects.existOrFail('dscViewModeToggle'); + await testSubjects.missingOrFail('dscViewModeToggle'); + await testSubjects.existOrFail('discoverQueryTotalHits'); if (!useLegacyTable) { await testSubjects.existOrFail('unifiedDataTableToolbar'); diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index d565c5168641b..315ecba11c6d5 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -41,6 +41,7 @@ export class VisualizePageObject extends FtrService { private readonly elasticChart = this.ctx.getService('elasticChart'); private readonly common = this.ctx.getPageObject('common'); private readonly header = this.ctx.getPageObject('header'); + private readonly timePicker = this.ctx.getPageObject('timePicker'); private readonly visChart = this.ctx.getPageObject('visChart'); private readonly toasts = this.ctx.getService('toasts'); @@ -63,6 +64,7 @@ export class VisualizePageObject extends FtrService { [FORMATS_UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', 'visualization:visualize:legacyHeatmapChartsLibrary': isLegacyChart, 'histogram:maxBars': 100, + 'timepicker:timeDefaults': `{ "from": "${this.timePicker.defaultStartTimeUTC}", "to": "${this.timePicker.defaultEndTimeUTC}"}`, }); } diff --git a/tsconfig.base.json b/tsconfig.base.json index 68faf44ed74d4..a525823e98e9d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1034,6 +1034,8 @@ "@kbn/import-locator/*": ["packages/kbn-import-locator/*"], "@kbn/import-resolver": ["packages/kbn-import-resolver"], "@kbn/import-resolver/*": ["packages/kbn-import-resolver/*"], + "@kbn/index-adapter": ["packages/kbn-index-adapter"], + "@kbn/index-adapter/*": ["packages/kbn-index-adapter/*"], "@kbn/index-lifecycle-management-common-shared": ["x-pack/packages/index-lifecycle-management/index_lifecycle_management_common_shared"], "@kbn/index-lifecycle-management-common-shared/*": ["x-pack/packages/index-lifecycle-management/index_lifecycle_management_common_shared/*"], "@kbn/index-lifecycle-management-plugin": ["x-pack/plugins/index_lifecycle_management"], diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx index 23bdbdaea3593..fac7c77570673 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx @@ -99,7 +99,7 @@ export function ChatItem({ const [editing, setEditing] = useState(false); const [expanded, setExpanded] = useState(Boolean(element)); - const actions = [canCopy, collapsed, canCopy].filter(Boolean); + const actions = [canCopy, collapsed].filter(Boolean); const noBodyMessageClassName = css` ${moreCompactHeaderClassName} @@ -182,7 +182,7 @@ export function ChatItem({ /> } className={ - actions.length === 0 && !content + actions.length === 0 && !content && !element ? noPanelMessageClassName : collapsed ? noBodyMessageClassName diff --git a/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx index 6a304430103ab..7dcf9cadb6bbf 100644 --- a/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx @@ -247,7 +247,7 @@ describe('getTimelineItemsFromConversation', () => { expect(pick(items[3], 'actions', 'display')).toEqual({ actions: { - canCopy: true, + canCopy: false, canEdit: false, canGiveFeedback: false, canRegenerate: false, diff --git a/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx index 999ac4f095025..5160e8b636b6c 100644 --- a/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx @@ -130,7 +130,6 @@ export function getTimelineItemsfromConversation({ switch (role) { case MessageRole.User: - actions.canCopy = true; actions.canGiveFeedback = false; actions.canRegenerate = false; @@ -210,11 +209,16 @@ export function getTimelineItemsfromConversation({ display.collapsed = false; } + if (!content) { + actions.canCopy = false; + } else { + actions.canCopy = true; + } + break; case MessageRole.Assistant: actions.canRegenerate = hasConnector; - actions.canCopy = true; actions.canGiveFeedback = true; display.hide = false; @@ -250,6 +254,13 @@ export function getTimelineItemsfromConversation({ display.collapsed = false; actions.canEdit = false; } + + if (!content) { + actions.canCopy = false; + } else { + actions.canCopy = true; + } + break; } diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx index 8f7c5e29ec3fe..58ecea3bc3bea 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx @@ -128,7 +128,7 @@ export const SvgDefsMarker = () => { const { euiTheme } = useEuiTheme(); return ( - + diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index eca9872d73897..0b956cb19e10d 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { useMemo, useRef, useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; +import { size, isEmpty, isEqual, xorWith } from 'lodash'; import { Background, Controls, @@ -14,7 +15,8 @@ import { useEdgesState, useNodesState, } from '@xyflow/react'; -import type { Edge, Node } from '@xyflow/react'; +import type { Edge, FitViewOptions, Node, ReactFlowInstance } from '@xyflow/react'; +import { useGeneratedHtmlId } from '@elastic/eui'; import type { CommonProps } from '@elastic/eui'; import { SvgDefsMarker } from '../edge/styles'; import { @@ -33,9 +35,23 @@ import type { EdgeViewModel, NodeViewModel } from '../types'; import '@xyflow/react/dist/style.css'; export interface GraphProps extends CommonProps { + /** + * Array of node view models to be rendered in the graph. + */ nodes: NodeViewModel[]; + /** + * Array of edge view models to be rendered in the graph. + */ edges: EdgeViewModel[]; + /** + * Determines whether the graph is interactive (allows panning, zooming, etc.). + * When set to false, the graph is locked and user interactions are disabled, effectively putting it in view-only mode. + */ interactive: boolean; + /** + * Determines whether the graph is locked. Nodes and edges are still interactive, but the graph itself is not. + */ + isLocked?: boolean; } const nodeTypes = { @@ -66,28 +82,47 @@ const edgeTypes = { * * @returns {JSX.Element} The rendered Graph component. */ -export const Graph: React.FC = ({ nodes, edges, interactive, ...rest }) => { - const layoutCalled = useRef(false); - const [isGraphLocked, setIsGraphLocked] = useState(interactive); - const { initialNodes, initialEdges } = useMemo( - () => processGraph(nodes, edges, isGraphLocked), - [nodes, edges, isGraphLocked] - ); - - const [nodesState, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edgesState, _setEdges, onEdgesChange] = useEdgesState(initialEdges); - - if (!layoutCalled.current) { - const { nodes: layoutedNodes } = layoutGraph(nodesState, edgesState); - setNodes(layoutedNodes); - layoutCalled.current = true; - } +export const Graph: React.FC = ({ + nodes, + edges, + interactive, + isLocked = false, + ...rest +}) => { + const backgroundId = useGeneratedHtmlId(); + const fitViewRef = useRef< + ((fitViewOptions?: FitViewOptions | undefined) => Promise) | null + >(null); + const currNodesRef = useRef([]); + const currEdgesRef = useRef([]); + const [isGraphInteractive, setIsGraphInteractive] = useState(interactive); + const [nodesState, setNodes, onNodesChange] = useNodesState>([]); + const [edgesState, setEdges, onEdgesChange] = useEdgesState>([]); + + useEffect(() => { + // On nodes or edges changes reset the graph and re-layout + if ( + !isArrayOfObjectsEqual(nodes, currNodesRef.current) || + !isArrayOfObjectsEqual(edges, currEdgesRef.current) + ) { + const { initialNodes, initialEdges } = processGraph(nodes, edges, isGraphInteractive); + const { nodes: layoutedNodes } = layoutGraph(initialNodes, initialEdges); + + setNodes(layoutedNodes); + setEdges(initialEdges); + currNodesRef.current = nodes; + currEdgesRef.current = edges; + setTimeout(() => { + fitViewRef.current?.(); + }, 30); + } + }, [nodes, edges, setNodes, setEdges, isGraphInteractive]); const onInteractiveStateChange = useCallback( (interactiveStatus: boolean): void => { - setIsGraphLocked(interactiveStatus); - setNodes((prevNodes) => - prevNodes.map((node) => ({ + setIsGraphInteractive(interactiveStatus); + setNodes((currNodes) => + currNodes.map((node) => ({ ...node, data: { ...node.data, @@ -99,23 +134,29 @@ export const Graph: React.FC = ({ nodes, edges, interactive, ...rest [setNodes] ); + const onInitCallback = useCallback( + (xyflow: ReactFlowInstance, Edge>) => { + window.requestAnimationFrame(() => xyflow.fitView()); + fitViewRef.current = xyflow.fitView; + + // When the graph is not initialized as interactive, we need to fit the view on resize + if (!interactive) { + const resizeObserver = new ResizeObserver(() => { + xyflow.fitView(); + }); + resizeObserver.observe(document.querySelector('.react-flow') as Element); + return () => resizeObserver.disconnect(); + } + }, + [interactive] + ); + return (
{ - window.requestAnimationFrame(() => xyflow.fitView()); - - // When the graph is not initialized as interactive, we need to fit the view on resize - if (!interactive) { - const resizeObserver = new ResizeObserver(() => { - xyflow.fitView(); - }); - resizeObserver.observe(document.querySelector('.react-flow') as Element); - return () => resizeObserver.disconnect(); - } - }} + onInit={onInitCallback} nodeTypes={nodeTypes} edgeTypes={edgeTypes} nodes={nodesState} @@ -123,16 +164,17 @@ export const Graph: React.FC = ({ nodes, edges, interactive, ...rest onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} proOptions={{ hideAttribution: true }} - panOnDrag={isGraphLocked} - zoomOnScroll={isGraphLocked} - zoomOnPinch={isGraphLocked} - zoomOnDoubleClick={isGraphLocked} - preventScrolling={isGraphLocked} - nodesDraggable={interactive && isGraphLocked} + panOnDrag={isGraphInteractive && !isLocked} + zoomOnScroll={isGraphInteractive && !isLocked} + zoomOnPinch={isGraphInteractive && !isLocked} + zoomOnDoubleClick={isGraphInteractive && !isLocked} + preventScrolling={interactive} + nodesDraggable={interactive && isGraphInteractive && !isLocked} maxZoom={1.3} + minZoom={0.1} > {interactive && } - + {' '}
); @@ -173,32 +215,41 @@ const processGraph = ( return node; }); - const initialEdges: Array> = edgesModel.map((edgeData) => { - const isIn = - nodesById[edgeData.source].shape !== 'label' && nodesById[edgeData.target].shape === 'group'; - const isInside = - nodesById[edgeData.source].shape === 'group' && nodesById[edgeData.target].shape === 'label'; - const isOut = - nodesById[edgeData.source].shape === 'label' && nodesById[edgeData.target].shape === 'group'; - const isOutside = - nodesById[edgeData.source].shape === 'group' && nodesById[edgeData.target].shape !== 'label'; - - return { - id: edgeData.id, - type: 'default', - source: edgeData.source, - sourceHandle: isInside ? 'inside' : isOutside ? 'outside' : undefined, - target: edgeData.target, - targetHandle: isIn ? 'in' : isOut ? 'out' : undefined, - focusable: false, - selectable: false, - data: { - ...edgeData, - sourceShape: nodesById[edgeData.source].shape, - targetShape: nodesById[edgeData.target].shape, - }, - }; - }); + const initialEdges: Array> = edgesModel + .filter((edgeData) => nodesById[edgeData.source] && nodesById[edgeData.target]) + .map((edgeData) => { + const isIn = + nodesById[edgeData.source].shape !== 'label' && + nodesById[edgeData.target].shape === 'group'; + const isInside = + nodesById[edgeData.source].shape === 'group' && + nodesById[edgeData.target].shape === 'label'; + const isOut = + nodesById[edgeData.source].shape === 'label' && + nodesById[edgeData.target].shape === 'group'; + const isOutside = + nodesById[edgeData.source].shape === 'group' && + nodesById[edgeData.target].shape !== 'label'; + + return { + id: edgeData.id, + type: 'default', + source: edgeData.source, + sourceHandle: isInside ? 'inside' : isOutside ? 'outside' : undefined, + target: edgeData.target, + targetHandle: isIn ? 'in' : isOut ? 'out' : undefined, + focusable: false, + selectable: false, + data: { + ...edgeData, + sourceShape: nodesById[edgeData.source].shape, + targetShape: nodesById[edgeData.target].shape, + }, + }; + }); return { initialNodes, initialEdges }; }; + +const isArrayOfObjectsEqual = (x: object[], y: object[]) => + size(x) === size(y) && isEmpty(xorWith(x, y, isEqual)); diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.stories.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.stories.tsx new file mode 100644 index 0000000000000..6d5b3c1b372fc --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.stories.tsx @@ -0,0 +1,209 @@ +/* + * 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, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ThemeProvider } from '@emotion/react'; +import { Story } from '@storybook/react'; +import { css } from '@emotion/react'; +import { EuiListGroup, EuiHorizontalRule } from '@elastic/eui'; +import type { EntityNodeViewModel, NodeProps } from '..'; +import { Graph } from '..'; +import { GraphPopover } from './graph_popover'; +import { ExpandButtonClickCallback } from '../types'; +import { useGraphPopover } from './use_graph_popover'; +import { ExpandPopoverListItem } from '../styles'; + +export default { + title: 'Components/Graph Components/Graph Popovers', + description: 'CDR - Graph visualization', + argTypes: {}, +}; + +const useExpandButtonPopover = () => { + const { id, state, actions } = useGraphPopover('node-expand-popover'); + const { openPopover, closePopover } = actions; + + const selectedNode = useRef(null); + const unToggleCallbackRef = useRef<(() => void) | null>(null); + const [pendingOpen, setPendingOpen] = useState<{ + node: NodeProps; + el: HTMLElement; + unToggleCallback: () => void; + } | null>(null); + + const onNodeExpandButtonClick: ExpandButtonClickCallback = useCallback( + (e, node, unToggleCallback) => { + if (selectedNode.current?.id === node.id) { + // If the same node is clicked again, close the popover + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + closePopover(); + } else { + // Close the current popover if open + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + + // Set the pending open state + setPendingOpen({ node, el: e.currentTarget, unToggleCallback }); + + closePopover(); + } + }, + [closePopover] + ); + + useEffect(() => { + if (!state.isOpen && pendingOpen) { + const { node, el, unToggleCallback } = pendingOpen; + + selectedNode.current = node; + unToggleCallbackRef.current = unToggleCallback; + openPopover(el); + + setPendingOpen(null); + } + }, [state.isOpen, pendingOpen, openPopover]); + + const closePopoverHandler = useCallback(() => { + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + closePopover(); + }, [closePopover]); + + const PopoverComponent = memo(() => ( + + + {}} + /> + {}} + /> + {}} + /> + + {}} /> + + + )); + + const actionsWithClose = useMemo( + () => ({ + ...actions, + closePopover: closePopoverHandler, + }), + [actions, closePopoverHandler] + ); + + return useMemo( + () => ({ + onNodeExpandButtonClick, + Popover: PopoverComponent, + id, + actions: actionsWithClose, + state, + }), + [PopoverComponent, actionsWithClose, id, onNodeExpandButtonClick, state] + ); +}; + +const useNodePopover = () => { + const { id, state, actions } = useGraphPopover('node-popover'); + + const PopoverComponent = memo(() => ( + + TODO + + )); + + return useMemo( + () => ({ + onNodeClick: (e: React.MouseEvent) => actions.openPopover(e.currentTarget), + Popover: PopoverComponent, + id, + actions, + state, + }), + [PopoverComponent, actions, id, state] + ); +}; + +const Template: Story = () => { + const expandNodePopover = useExpandButtonPopover(); + const nodePopover = useNodePopover(); + const popovers = [expandNodePopover, nodePopover]; + const isPopoverOpen = popovers.some((popover) => popover.state.isOpen); + + const popoverOpenWrapper = (cb: Function, ...args: any[]) => { + [expandNodePopover.actions.closePopover, nodePopover.actions.closePopover].forEach( + (closePopover) => { + closePopover(); + } + ); + cb.apply(null, args); + }; + + const expandButtonClickHandler = (...args: any[]) => + popoverOpenWrapper(expandNodePopover.onNodeExpandButtonClick, ...args); + const nodeClickHandler = (...args: any[]) => popoverOpenWrapper(nodePopover.onNodeClick, ...args); + + const nodes: EntityNodeViewModel[] = useMemo( + () => + (['hexagon', 'ellipse', 'rectangle', 'pentagon', 'diamond'] as const).map((shape, idx) => ({ + id: `${idx}`, + label: `Node ${idx}`, + color: 'primary', + icon: 'okta', + interactive: true, + shape, + expandButtonClick: expandButtonClickHandler, + nodeClick: nodeClickHandler, + })), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( + + + {popovers?.map((popover) => popover.Popover && )} + + ); +}; + +export const GraphPopovers = Template.bind({}); diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx new file mode 100644 index 0000000000000..570c1332a8834 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx @@ -0,0 +1,85 @@ +/* + * 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, { type PropsWithChildren } from 'react'; +import type { CommonProps, EuiWrappingPopoverProps } from '@elastic/eui'; +import { EuiWrappingPopover, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export interface GraphPopoverProps + extends PropsWithChildren, + CommonProps, + Pick< + EuiWrappingPopoverProps, + 'anchorPosition' | 'panelClassName' | 'panelPaddingSize' | 'panelStyle' + > { + isOpen: boolean; + anchorElement: HTMLElement | null; + closePopover: () => void; +} + +export const GraphPopover: React.FC = ({ + isOpen, + anchorElement, + closePopover, + children, + ...rest +}) => { + const { euiTheme } = useEuiTheme(); + + if (!anchorElement) { + return null; + } + + return ( + { + anchorElement.focus(); + return false; + }, + preventScrollOnFocus: true, + onClickOutside: () => { + closePopover(); + }, + }} + > + {children} + + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx new file mode 100644 index 0000000000000..f5bca30d1e5ae --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useMemo, useState } from 'react'; + +export interface PopoverActions { + openPopover: (anchorElement: HTMLElement) => void; + closePopover: () => void; +} + +export interface PopoverState { + isOpen: boolean; + anchorElement: HTMLElement | null; +} + +export interface GraphPopoverState { + id: string; + actions: PopoverActions; + state: PopoverState; +} + +export const useGraphPopover = (id: string): GraphPopoverState => { + const [isOpen, setIsOpen] = useState(false); + const [anchorElement, setAnchorElement] = useState(null); + + // Memoize actions to prevent them from changing on re-renders + const openPopover = useCallback((anchor: HTMLElement) => { + setAnchorElement(anchor); + setIsOpen(true); + }, []); + + const closePopover = useCallback(() => { + setIsOpen(false); + setAnchorElement(null); + }, []); + + // Memoize the context values + const actions: PopoverActions = useMemo( + () => ({ openPopover, closePopover }), + [openPopover, closePopover] + ); + + const state: PopoverState = useMemo(() => ({ isOpen, anchorElement }), [isOpen, anchorElement]); + + return useMemo( + () => ({ + id, + actions, + state, + }), + [id, actions, state] + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts index 5b2f8d71323bb..2b050aa55429f 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts @@ -6,6 +6,8 @@ */ export { Graph } from './graph/graph'; +export { GraphPopover } from './graph/graph_popover'; +export { useGraphPopover } from './graph/use_graph_popover'; export type { GraphProps } from './graph/graph'; export type { NodeViewModel, diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx index f96068061a433..75ad989b625e8 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx @@ -19,12 +19,13 @@ import { HandleStyleOverride, } from './styles'; import { DiamondHoverShape, DiamondShape } from './shapes/diamond_shape'; +import { NodeExpandButton } from './node_expand_button'; const NODE_WIDTH = 99; const NODE_HEIGHT = 98; export const DiamondNode: React.FC = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -55,11 +56,14 @@ export const DiamondNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2 - 4}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 4}px`} + /> + )} = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -55,11 +56,14 @@ export const EllipseNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 2}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} + /> + )} = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -55,11 +56,14 @@ export const HexagonNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 2 + 2}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2 - 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2 + 2}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 2}px`} + /> + )} , unToggleCallback: () => void) => void; +} + +export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { + // State to track whether the icon is "plus" or "minus" + const [isToggled, setIsToggled] = useState(false); + + const unToggleCallback = useCallback(() => { + setIsToggled(false); + }, []); + + const onClickHandler = (e: React.MouseEvent) => { + setIsToggled((currIsToggled) => !currIsToggled); + onClick?.(e, unToggleCallback); + }; + + return ( + + + + ); +}; + +NodeExpandButton.ExpandButtonSize = ExpandButtonSize; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx index f2282e9fa2d7d..f2745cef7ec80 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx @@ -20,6 +20,7 @@ import { } from './styles'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { PentagonHoverShape, PentagonShape } from './shapes/pentagon_shape'; +import { NodeExpandButton } from './node_expand_button'; const PentagonShapeOnHover = styled(NodeShapeOnHoverSvg)` transform: translate(-50%, -51.5%); @@ -29,7 +30,7 @@ const NODE_WIDTH = 91; const NODE_HEIGHT = 88; export const PentagonNode: React.FC = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -60,11 +61,14 @@ export const PentagonNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 2}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} + /> + )} = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -55,11 +56,14 @@ export const RectangleNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 4}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize / 2) / 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 4}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize / 2) / 2}px`} + /> + )} ) => void; +} + +export const NodeButton: React.FC = ({ onClick }) => ( + + + +); + +const StyledNodeContainer = styled.div` + position: absolute; + width: ${NODE_WIDTH}px; + height: ${NODE_HEIGHT}px; + z-index: 1; +`; + +const StyledNodeButton = styled.div` + width: ${NODE_WIDTH}px; + height: ${NODE_HEIGHT}px; +`; + +export const StyledNodeExpandButton = styled.div` + opacity: 0; /* Hidden by default */ + transition: opacity 0.2s ease; /* Smooth transition */ + ${(props: NodeExpandButtonProps) => + (Boolean(props.x) || Boolean(props.y)) && + `transform: translate(${props.x ?? '0'}, ${props.y ?? '0'});`} + position: absolute; + z-index: 1; + + &.toggled { + opacity: 1; + } + + ${NodeShapeContainer}:hover & { + opacity: 1; /* Show on hover */ + } + + &:has(button:focus) { + opacity: 1; /* Show when button is active */ + } + + .react-flow__node:focus:focus-visible & { + opacity: 1; /* Show on node focus */ + } +`; + export const NodeShapeOnHoverSvg = styled(NodeShapeSvg)` opacity: 0; /* Hidden by default */ transition: opacity 0.2s ease; /* Smooth transition */ @@ -108,6 +157,10 @@ export const NodeShapeOnHoverSvg = styled(NodeShapeSvg)` opacity: 1; /* Show on hover */ } + ${NodeShapeContainer}:has(${StyledNodeExpandButton}.toggled) & { + opacity: 1; /* Show on hover */ + } + .react-flow__node:focus:focus-visible & { opacity: 1; /* Show on hover */ } @@ -145,9 +198,9 @@ NodeLabel.defaultProps = { textAlign: 'center', }; -const ExpandButtonSize = 18; +export const ExpandButtonSize = 18; -const RoundEuiButtonIcon = styled(EuiButtonIcon)` +export const RoundEuiButtonIcon = styled(EuiButtonIcon)` border-radius: 50%; background-color: ${(_props) => useEuiBackgroundColor('plain')}; width: ${ExpandButtonSize}px; @@ -164,57 +217,6 @@ const RoundEuiButtonIcon = styled(EuiButtonIcon)` } `; -export const StyledNodeButton = styled.div` - opacity: 0; /* Hidden by default */ - transition: opacity 0.2s ease; /* Smooth transition */ - ${(props: NodeButtonProps) => - (Boolean(props.x) || Boolean(props.y)) && - `transform: translate(${props.x ?? '0'}, ${props.y ?? '0'});`} - position: absolute; - z-index: 1; - - ${NodeShapeContainer}:hover & { - opacity: 1; /* Show on hover */ - } - - &:has(button:focus) { - opacity: 1; /* Show when button is active */ - } - - .react-flow__node:focus:focus-visible & { - opacity: 1; /* Show on node focus */ - } -`; - -export interface NodeButtonProps { - x?: string; - y?: string; - onClick?: (e: React.MouseEvent) => void; -} - -export const NodeButton = ({ x, y, onClick }: NodeButtonProps) => { - // State to track whether the icon is "plus" or "minus" - const [isToggled, setIsToggled] = useState(false); - - const onClickHandler = (e: React.MouseEvent) => { - setIsToggled(!isToggled); - onClick?.(e); - }; - - return ( - - - - ); -}; - -NodeButton.ExpandButtonSize = ExpandButtonSize; - export const HandleStyleOverride: React.CSSProperties = { background: 'none', border: 'none', diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx new file mode 100644 index 0000000000000..0efff1c88456c --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiIcon, + useEuiBackgroundColor, + useEuiTheme, + type EuiIconProps, + type _EuiBackgroundColor, + EuiListGroupItemProps, + EuiListGroupItem, + EuiText, +} from '@elastic/eui'; +import styled from '@emotion/styled'; + +interface EuiColorProps { + color: keyof ReturnType['euiTheme']['colors']; + background: _EuiBackgroundColor; +} + +type IconContainerProps = EuiColorProps; + +const IconContainer = styled.div` + position: relative; + width: 24px; + height: 24px; + border-radius: 50%; + color: ${({ color }) => { + const { euiTheme } = useEuiTheme(); + return euiTheme.colors[color]; + }}; + background-color: ${({ background }) => useEuiBackgroundColor(background)}; + border: 1px solid + ${({ color }) => { + const { euiTheme } = useEuiTheme(); + return euiTheme.colors[color]; + }}; + margin-right: 8px; +`; + +const StyleEuiIcon = styled(EuiIcon)` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +type RoundedEuiIconProps = EuiIconProps & EuiColorProps; + +const RoundedEuiIcon: React.FC = ({ color, background, ...rest }) => ( + + + +); + +export const ExpandPopoverListItem: React.FC< + Pick +> = (props) => { + const { euiTheme } = useEuiTheme(); + return ( + + ) : undefined + } + label={ + + {props.label} + + } + onClick={props.onClick} + /> + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts index 27ec18f35f45b..328829ee3fabe 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import type { EntityNodeDataModel, GroupNodeDataModel, @@ -24,11 +25,20 @@ interface BaseNodeDataViewModel { interactive?: boolean; } +export type NodeClickCallback = (e: React.MouseEvent, node: NodeProps) => void; + +export type ExpandButtonClickCallback = ( + e: React.MouseEvent, + node: NodeProps, + unToggleCallback: () => void +) => void; + export interface EntityNodeViewModel extends Record, EntityNodeDataModel, BaseNodeDataViewModel { - expandButtonClick?: (e: React.MouseEvent, node: NodeProps) => void; + expandButtonClick?: ExpandButtonClickCallback; + nodeClick?: NodeClickCallback; } export interface GroupNodeViewModel @@ -40,7 +50,7 @@ export interface LabelNodeViewModel extends Record, LabelNodeDataModel, BaseNodeDataViewModel { - expandButtonClick?: (e: React.MouseEvent, node: NodeProps) => void; + expandButtonClick?: ExpandButtonClickCallback; } export type NodeViewModel = EntityNodeViewModel | GroupNodeViewModel | LabelNodeViewModel; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts index 54c24f6ce7b8f..d883dfe98d564 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts @@ -19,6 +19,5 @@ export type AssistantFeatureKey = keyof AssistantFeatures; * Default features available to the elastic assistant */ export const defaultAssistantFeatures = Object.freeze({ - assistantKnowledgeBaseByDefault: true, assistantModelEvaluation: false, }); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts index b3ab7cca5bc02..0f8b6235d7dc9 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts @@ -18,6 +18,5 @@ import { z } from '@kbn/zod'; export type GetCapabilitiesResponse = z.infer; export const GetCapabilitiesResponse = z.object({ - assistantKnowledgeBaseByDefault: z.boolean(), assistantModelEvaluation: z.boolean(), }); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml index 01b5eb0e15823..a042abd391796 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml @@ -20,12 +20,9 @@ paths: schema: type: object properties: - assistantKnowledgeBaseByDefault: - type: boolean assistantModelEvaluation: type: boolean required: - - assistantKnowledgeBaseByDefault - assistantModelEvaluation '400': description: Generic Error diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts index 4f03dbe0b1343..a4f38cafd460b 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts @@ -55,20 +55,6 @@ export type CreateKnowledgeBaseRequestParamsInput = z.input< export type CreateKnowledgeBaseResponse = z.infer; export const CreateKnowledgeBaseResponse = KnowledgeBaseResponse; -export type DeleteKnowledgeBaseRequestParams = z.infer; -export const DeleteKnowledgeBaseRequestParams = z.object({ - /** - * The KnowledgeBase `resource` value. - */ - resource: z.string().optional(), -}); -export type DeleteKnowledgeBaseRequestParamsInput = z.input< - typeof DeleteKnowledgeBaseRequestParams ->; - -export type DeleteKnowledgeBaseResponse = z.infer; -export const DeleteKnowledgeBaseResponse = KnowledgeBaseResponse; - export type ReadKnowledgeBaseRequestParams = z.infer; export const ReadKnowledgeBaseRequestParams = z.object({ /** diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml index b4c16189e2387..67193212abb49 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml @@ -100,40 +100,6 @@ paths: type: string message: type: string - delete: - x-codegen-enabled: true - x-labels: [ess, serverless] - operationId: DeleteKnowledgeBase - description: Deletes KnowledgeBase with the `resource` field. - summary: Deletes a KnowledgeBase - tags: - - KnowledgeBase API - parameters: - - name: resource - in: path - description: The KnowledgeBase `resource` value. - schema: - type: string - responses: - 200: - description: Indicates a successful call. - content: - application/json: - schema: - $ref: '#/components/schemas/KnowledgeBaseResponse' - 400: - description: Generic Error - content: - application/json: - schema: - type: object - properties: - statusCode: - type: number - error: - type: string - message: - type: string components: schemas: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.schema.yaml index 7670114c7164a..db68416b14561 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.schema.yaml @@ -6,7 +6,7 @@ paths: /internal/elastic_assistant/knowledge_base/entries/_bulk_action: post: x-codegen-enabled: true - # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + # Targeted to update to public by 8.18 x-internal: true x-labels: [ess, serverless] operationId: PerformKnowledgeBaseEntryBulkAction diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml index 7479b5cca8225..10105ef7dce90 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml @@ -6,7 +6,7 @@ paths: /internal/elastic_assistant/knowledge_base/entries: post: x-codegen-enabled: true - # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + # Targeted to update to public by 8.18 x-internal: true x-labels: [ess, serverless] operationId: CreateKnowledgeBaseEntry @@ -37,7 +37,7 @@ paths: /internal/elastic_assistant/knowledge_base/entries/{id}: get: x-codegen-enabled: true - # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + # Targeted to update to public by 8.18 x-internal: true x-labels: [ess, serverless] operationId: ReadKnowledgeBaseEntry @@ -67,7 +67,7 @@ paths: $ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryErrorSchema' put: x-codegen-enabled: true - # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + # Targeted to update to public by 8.18 x-internal: true x-labels: [ess, serverless] operationId: UpdateKnowledgeBaseEntry @@ -103,7 +103,7 @@ paths: $ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryErrorSchema' delete: x-codegen-enabled: true - # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + # Targeted to update to public by 8.18 x-internal: true x-labels: [ess, serverless] operationId: DeleteKnowledgeBaseEntry diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.schema.yaml index 8794a94b0efc9..9b9696e8760fc 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.schema.yaml @@ -6,7 +6,7 @@ paths: /internal/elastic_assistant/knowledge_base/entries/_find: get: x-codegen-enabled: true - # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + # Targeted to update to public by 8.18 x-internal: true x-labels: [ess, serverless] operationId: FindKnowledgeBaseEntries diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.test.tsx index 5509f43037444..2a1ffc5072570 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.test.tsx @@ -7,12 +7,7 @@ import { HttpSetup } from '@kbn/core-http-browser'; -import { - deleteKnowledgeBase, - getKnowledgeBaseIndices, - getKnowledgeBaseStatus, - postKnowledgeBase, -} from './api'; +import { getKnowledgeBaseIndices, getKnowledgeBaseStatus, postKnowledgeBase } from './api'; jest.mock('@kbn/core-http-browser'); @@ -78,29 +73,6 @@ describe('API tests', () => { }); }); - describe('deleteKnowledgeBase', () => { - it('calls the knowledge base API when correct resource path', async () => { - await deleteKnowledgeBase(knowledgeBaseArgs); - - expect(mockHttp.fetch).toHaveBeenCalledWith( - '/internal/elastic_assistant/knowledge_base/a-resource', - { - method: 'DELETE', - signal: undefined, - version: '1', - } - ); - }); - it('returns error when error is an error', async () => { - const error = 'simulated error'; - (mockHttp.fetch as jest.Mock).mockImplementation(() => { - throw new Error(error); - }); - - await expect(deleteKnowledgeBase(knowledgeBaseArgs)).resolves.toThrowError('simulated error'); - }); - }); - describe('getKnowledgeBaseIndices', () => { it('calls the knowledge base API when correct resource path', async () => { await getKnowledgeBaseIndices({ http: mockHttp }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.tsx index 4db8c0787a1e1..00fe022ad9517 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/api.tsx @@ -9,8 +9,6 @@ import { API_VERSIONS, CreateKnowledgeBaseRequestParams, CreateKnowledgeBaseResponse, - DeleteKnowledgeBaseRequestParams, - DeleteKnowledgeBaseResponse, ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_INDICES_URL, ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL, GetKnowledgeBaseIndicesResponse, @@ -79,38 +77,6 @@ export const postKnowledgeBase = async ({ return response as CreateKnowledgeBaseResponse; }; -/** - * API call for deleting the Knowledge Base. Provide a resource to delete that specific resource. - * - * @param {Object} options - The options object. - * @param {HttpSetup} options.http - HttpSetup - * @param {string} [options.resource] - Resource to be deleted from the KB, otherwise delete the entire KB - * @param {AbortSignal} [options.signal] - AbortSignal - * - * @returns {Promise} - */ -export const deleteKnowledgeBase = async ({ - http, - resource, - signal, -}: DeleteKnowledgeBaseRequestParams & { - http: HttpSetup; - signal?: AbortSignal | undefined; -}): Promise => { - try { - const path = ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL.replace('{resource?}', resource || ''); - const response = await http.fetch(path, { - method: 'DELETE', - signal, - version: API_VERSIONS.internal.v1, - }); - - return response as DeleteKnowledgeBaseResponse; - } catch (error) { - return error as IHttpFetchError; - } -}; - /** * API call for getting indices that have fields of `semantic_text` type. * diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_delete_knowledge_base.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_delete_knowledge_base.test.tsx deleted file mode 100644 index b50c345edb3b3..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_delete_knowledge_base.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act, renderHook } from '@testing-library/react-hooks'; -import { useDeleteKnowledgeBase, UseDeleteKnowledgeBaseParams } from './use_delete_knowledge_base'; -import { deleteKnowledgeBase as _deleteKnowledgeBase } from './api'; -import { useMutation as _useMutation } from '@tanstack/react-query'; - -const useMutationMock = _useMutation as jest.Mock; -const deleteKnowledgeBaseMock = _deleteKnowledgeBase as jest.Mock; - -jest.mock('./api', () => { - const actual = jest.requireActual('./api'); - return { - ...actual, - deleteKnowledgeBase: jest.fn((...args) => actual.deleteKnowledgeBase(...args)), - }; -}); -jest.mock('./use_knowledge_base_status'); - -jest.mock('@tanstack/react-query', () => ({ - useMutation: jest.fn().mockImplementation(async (queryKey, fn, opts) => { - try { - const res = await fn(); - return Promise.resolve(res); - } catch (e) { - opts.onError(e); - } - }), -})); - -const statusResponse = { - success: true, -}; - -const http = { - fetch: jest.fn().mockResolvedValue(statusResponse), -}; -const toasts = { - addError: jest.fn(), -}; -const defaultProps = { http, toasts } as unknown as UseDeleteKnowledgeBaseParams; - -describe('useDeleteKnowledgeBase', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('should call api to delete knowledge base', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps)); - await waitForNextUpdate(); - - expect(defaultProps.http.fetch).toHaveBeenCalledWith( - '/internal/elastic_assistant/knowledge_base/', - { - method: 'DELETE', - signal: undefined, - version: '1', - } - ); - expect(toasts.addError).not.toHaveBeenCalled(); - }); - }); - it('should call api to delete knowledge base with resource arg', async () => { - useMutationMock.mockImplementation(async (queryKey, fn, opts) => { - try { - const res = await fn('something'); - return Promise.resolve(res); - } catch (e) { - opts.onError(e); - } - }); - await act(async () => { - const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps)); - await waitForNextUpdate(); - - expect(defaultProps.http.fetch).toHaveBeenCalledWith( - '/internal/elastic_assistant/knowledge_base/something', - { - method: 'DELETE', - signal: undefined, - version: '1', - } - ); - }); - }); - - it('should return delete response', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps)); - await waitForNextUpdate(); - - await expect(result.current).resolves.toStrictEqual(statusResponse); - }); - }); - - it('should display error toast when api throws error', async () => { - deleteKnowledgeBaseMock.mockRejectedValue(new Error('this is an error')); - await act(async () => { - const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps)); - await waitForNextUpdate(); - - expect(toasts.addError).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_delete_knowledge_base.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_delete_knowledge_base.tsx deleted file mode 100644 index 5e4ce82bde3bd..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_delete_knowledge_base.tsx +++ /dev/null @@ -1,58 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMutation } from '@tanstack/react-query'; -import type { IToasts } from '@kbn/core-notifications-browser'; -import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; -import { i18n } from '@kbn/i18n'; -import { deleteKnowledgeBase } from './api'; -import { useInvalidateKnowledgeBaseStatus } from './use_knowledge_base_status'; - -const DELETE_KNOWLEDGE_BASE_MUTATION_KEY = ['elastic-assistant', 'delete-knowledge-base']; - -export interface UseDeleteKnowledgeBaseParams { - http: HttpSetup; - toasts?: IToasts; -} - -/** - * Hook for deleting the Knowledge Base. Provide a resource name to delete a - * specific resource within KB. - * - * @param {Object} options - The options object. - * @param {HttpSetup} options.http - HttpSetup - * @param {IToasts} [options.toasts] - IToasts - * - * @returns {useMutation} hook for deleting the Knowledge Base - */ -export const useDeleteKnowledgeBase = ({ http, toasts }: UseDeleteKnowledgeBaseParams) => { - const invalidateKnowledgeBaseStatus = useInvalidateKnowledgeBaseStatus(); - return useMutation( - DELETE_KNOWLEDGE_BASE_MUTATION_KEY, - (resource?: string | void) => { - // Optional params workaround: see: https://github.com/TanStack/query/issues/1077#issuecomment-1431247266 - return deleteKnowledgeBase({ http, resource: resource ?? undefined }); - }, - { - onError: (error: IHttpFetchError) => { - if (error.name !== 'AbortError') { - toasts?.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { - title: i18n.translate('xpack.elasticAssistant.knowledgeBase.deleteError', { - defaultMessage: 'Error deleting Knowledge Base', - }), - } - ); - } - }, - onSettled: () => { - invalidateKnowledgeBaseStatus(); - }, - } - ); -}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index ef37506f2af17..406ef8be16c73 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -23,7 +23,7 @@ import { Conversation } from '../../..'; import { AssistantTitle } from '../assistant_title'; import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation'; -import { AssistantSettingsButton } from '../settings/assistant_settings_button'; +import { AssistantSettingsModal } from '../settings/assistant_settings_modal'; import * as i18n from './translations'; import { AIConnector } from '../../connectorland/connector_selector'; import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu'; @@ -113,7 +113,7 @@ export const AssistantHeader: React.FC = ({ > - { QUICK_PROMPTS_TAB, SYSTEM_PROMPTS_TAB, ])('%s', (tab) => { - it('Opens the tab on button click', () => { - (useAssistantContext as jest.Mock).mockImplementation(() => ({ - ...mockContext, - selectedSettingsTab: tab === CONVERSATIONS_TAB ? ANONYMIZATION_TAB : CONVERSATIONS_TAB, - })); - const { getByTestId } = render(, { - wrapper, - }); - fireEvent.click(getByTestId(`${tab}-button`)); - expect(setSelectedSettingsTab).toHaveBeenCalledWith(tab); - }); it('renders with the correct tab open', () => { (useAssistantContext as jest.Mock).mockImplementation(() => ({ ...mockContext, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx index 350780ea5b168..f325e411bae2b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx @@ -9,14 +9,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiButton, EuiButtonEmpty, - EuiIcon, EuiModal, EuiModalFooter, - EuiKeyPadMenu, - EuiKeyPadMenuItem, EuiPage, EuiPageBody, - EuiPageSidebar, EuiSplitPanel, } from '@elastic/eui'; @@ -80,16 +76,7 @@ export const AssistantSettings: React.FC = React.memo( conversations, conversationsLoaded, }) => { - const { - assistantFeatures: { - assistantModelEvaluation: modelEvaluatorEnabled, - assistantKnowledgeBaseByDefault, - }, - http, - toasts, - selectedSettingsTab, - setSelectedSettingsTab, - } = useAssistantContext(); + const { http, toasts, selectedSettingsTab, setSelectedSettingsTab } = useAssistantContext(); useEffect(() => { if (selectedSettingsTab == null) { @@ -214,115 +201,6 @@ export const AssistantSettings: React.FC = React.memo( return ( - {!assistantKnowledgeBaseByDefault && ( - - - setSelectedSettingsTab(CONVERSATIONS_TAB)} - data-test-subj={`${CONVERSATIONS_TAB}-button`} - > - <> - - - - - setSelectedSettingsTab(QUICK_PROMPTS_TAB)} - data-test-subj={`${QUICK_PROMPTS_TAB}-button`} - > - <> - - - - - setSelectedSettingsTab(SYSTEM_PROMPTS_TAB)} - data-test-subj={`${SYSTEM_PROMPTS_TAB}-button`} - > - - - - setSelectedSettingsTab(ANONYMIZATION_TAB)} - data-test-subj={`${ANONYMIZATION_TAB}-button`} - > - - - setSelectedSettingsTab(KNOWLEDGE_BASE_TAB)} - data-test-subj={`${KNOWLEDGE_BASE_TAB}-button`} - > - - - {modelEvaluatorEnabled && ( - setSelectedSettingsTab(EVALUATION_TAB)} - data-test-subj={`${EVALUATION_TAB}-button`} - > - - - )} - - - )} - ({ ), })); -describe('AssistantSettingsButton', () => { +describe('AssistantSettingsModal', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('Clicking the settings gear opens the conversations tab', () => { - const { getByTestId } = render(); - fireEvent.click(getByTestId('settings')); - expect(setSelectedSettingsTab).toHaveBeenCalledWith(CONVERSATIONS_TAB); - expect(setIsSettingsModalVisible).toHaveBeenCalledWith(true); - }); - it('Settings modal is visible and calls correct actions per click', () => { const { getByTestId } = render( - + ); fireEvent.click(getByTestId('on-close')); expect(setIsSettingsModalVisible).toHaveBeenCalledWith(false); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_modal.tsx similarity index 60% rename from x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_modal.tsx index 3d6544643ba3e..5f2d677adc9ee 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_modal.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback } from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { DataStreamApis } from '../use_data_stream_apis'; import { AIConnector } from '../../connectorland/connector_selector'; @@ -14,7 +13,6 @@ import { Conversation } from '../../..'; import { AssistantSettings } from './assistant_settings'; import * as i18n from './translations'; import { useAssistantContext } from '../../assistant_context'; -import { CONVERSATIONS_TAB } from './const'; interface Props { defaultConnector?: AIConnector; @@ -32,12 +30,11 @@ interface Props { } /** - * Gear button that opens the assistant settings modal + * Assistant settings modal */ -export const AssistantSettingsButton: React.FC = React.memo( +export const AssistantSettingsModal: React.FC = React.memo( ({ defaultConnector, - isDisabled = false, isSettingsModalVisible, setIsSettingsModalVisible, selectedConversationId, @@ -47,11 +44,7 @@ export const AssistantSettingsButton: React.FC = React.memo( refetchCurrentUserConversations, refetchPrompts, }) => { - const { - assistantFeatures: { assistantKnowledgeBaseByDefault }, - toasts, - setSelectedSettingsTab, - } = useAssistantContext(); + const { toasts } = useAssistantContext(); // Modal control functions const cleanupAndCloseModal = useCallback(() => { @@ -79,41 +72,20 @@ export const AssistantSettingsButton: React.FC = React.memo( [cleanupAndCloseModal, refetchCurrentUserConversations, refetchPrompts, toasts] ); - const handleShowConversationSettings = useCallback(() => { - setSelectedSettingsTab(CONVERSATIONS_TAB); - setIsSettingsModalVisible(true); - }, [setIsSettingsModalVisible, setSelectedSettingsTab]); - return ( - <> - {!assistantKnowledgeBaseByDefault && ( - - - - )} - - {isSettingsModalVisible && ( - - )} - + isSettingsModalVisible && ( + + ) ); } ); -AssistantSettingsButton.displayName = 'AssistantSettingsButton'; +AssistantSettingsModal.displayName = 'AssistantSettingsModal'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx index baed2ff4cdb86..7b55e994b47ad 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactElement, useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -32,11 +32,7 @@ interface Params { export const SettingsContextMenu: React.FC = React.memo( ({ isDisabled = false, onChatCleared }: Params) => { - const { - navigateToApp, - knowledgeBase, - assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault }, - } = useAssistantContext(); + const { navigateToApp, knowledgeBase } = useAssistantContext(); const [isPopoverOpen, setPopover] = useState(false); @@ -91,12 +87,11 @@ export const SettingsContextMenu: React.FC = React.memo( closePopover(); }, [closePopover, showAlertSettingsModal]); - // We are migrating away from the settings modal in favor of the new Stack Management UI - // Currently behind `assistantKnowledgeBaseByDefault` FF - const newItems: ReactElement[] = useMemo( + const items = useMemo( () => [ = React.memo( , = React.memo( , = React.memo( , = React.memo( , - ], - [ - handleNavigateToAnonymization, - handleNavigateToKnowledgeBase, - handleNavigateToSettings, - handleShowAlertsModal, - knowledgeBase.latestAlerts, - ] - ); - - const items = useMemo( - () => [ - ...(enableKnowledgeBaseByDefault ? newItems : []), = React.memo( , ], - [enableKnowledgeBaseByDefault, newItems, showDestroyModal] + [ + handleNavigateToAnonymization, + handleNavigateToKnowledgeBase, + handleNavigateToSettings, + handleShowAlertsModal, + knowledgeBase.latestAlerts, + showDestroyModal, + ] ); const handleReset = useCallback(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx index 763a2578ee273..b44dc682218d0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.test.tsx @@ -47,15 +47,6 @@ const defaultProps = { }, setUpdatedKnowledgeBaseSettings, }; -const mockDelete = jest.fn(); -jest.mock('../assistant/api/knowledge_base/use_delete_knowledge_base', () => ({ - useDeleteKnowledgeBase: jest.fn(() => { - return { - mutate: mockDelete, - isLoading: false, - }; - }), -})); const mockSetup = jest.fn(); jest.mock('../assistant/api/knowledge_base/use_setup_knowledge_base', () => ({ diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx index 180b88fc3cdc8..4900a6b0966e3 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx @@ -32,7 +32,6 @@ const mockContext = { http: { get: jest.fn(), }, - assistantFeatures: { assistantKnowledgeBaseByDefault: true }, selectedSettingsTab: null, assistantAvailability: { isAssistantEnabled: true, @@ -175,17 +174,6 @@ describe('KnowledgeBaseSettingsManagement', () => { isLoading: false, }); }); - it('renders old kb settings when enableKnowledgeBaseByDefault is not enabled', () => { - (useAssistantContext as jest.Mock).mockImplementation(() => ({ - ...mockContext, - assistantFeatures: { - assistantKnowledgeBaseByDefault: false, - }, - })); - render(, { wrapper }); - - expect(screen.getByTestId('knowledge-base-settings')).toBeInTheDocument(); - }); it('renders loading spinner when data is not fetched', () => { (useKnowledgeBaseStatus as jest.Mock).mockReturnValue({ data: {}, isFetched: false }); render(, { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx index 86b3594daa3cd..183e74a18247a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx @@ -48,7 +48,6 @@ import { Flyout } from '../../assistant/common/components/assistant_settings_man import { useFlyoutModalVisibility } from '../../assistant/common/components/assistant_settings_management/flyout/use_flyout_modal_visibility'; import { IndexEntryEditor } from './index_entry_editor'; import { DocumentEntryEditor } from './document_entry_editor'; -import { KnowledgeBaseSettings } from '../knowledge_base_settings'; import { SetupKnowledgeBaseButton } from '../setup_knowledge_base_button'; import { useDeleteKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries'; import { @@ -73,7 +72,6 @@ interface Params { export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ dataViews }) => { const { - assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault }, assistantAvailability: { hasManageGlobalKnowledgeBase, isAssistantEnabled }, http, toasts, @@ -162,7 +160,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d } = useKnowledgeBaseEntries({ http, toasts, - enabled: enableKnowledgeBaseByDefault && isAssistantEnabled, + enabled: isAssistantEnabled, isRefetching: kbStatus?.is_setup_in_progress, }); @@ -332,21 +330,6 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d } }, [createEntry, duplicateKBItem, resetStateAndCloseFlyout]); - if (!enableKnowledgeBaseByDefault) { - return ( - <> - - - - ); - } return ( <> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.test.tsx index 4dfd4657212f8..898a97ec2e233 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.test.tsx @@ -35,9 +35,6 @@ describe('Attack discovery tour', () => { jest.clearAllMocks(); (useAssistantContext as jest.Mock).mockReturnValue({ navigateToApp, - assistantFeatures: { - assistantKnowledgeBaseByDefault: true, - }, }); jest.mocked(useLocalStorage).mockReturnValue([ { @@ -68,25 +65,6 @@ describe('Attack discovery tour', () => { expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull(); }); - it('should not render any tour steps when knowledge base feature flag is not activated', () => { - (useAssistantContext as jest.Mock).mockReturnValue({ - navigateToApp, - assistantFeatures: { - assistantKnowledgeBaseByDefault: false, - }, - }); - render( - -

{'Hello world'}

-
, - { - wrapper: TestProviders, - } - ); - expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull(); - expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull(); - }); - it('should not render any tour steps when tour is on step 2 and page is not knowledge base', () => { jest.mocked(useLocalStorage).mockReturnValue([ { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx index f7ef0252147c0..8d71b4491a2fd 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx @@ -28,10 +28,7 @@ const KnowledgeBaseTourComp: React.FC<{ children?: EuiTourStepProps['children']; isKbSettingsPage?: boolean; }> = ({ children, isKbSettingsPage = false }) => { - const { - navigateToApp, - assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault }, - } = useAssistantContext(); + const { navigateToApp } = useAssistantContext(); const [tourState, setTourState] = useLocalStorage( NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, @@ -106,7 +103,7 @@ const KnowledgeBaseTourComp: React.FC<{ return () => clearTimeout(timer); }, []); - if (!enableKnowledgeBaseByDefault || isTestAutomation || !tourState?.isTourActive) { + if (isTestAutomation || !tourState?.isTourActive) { return children ?? null; } diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index 9ab02e0931d9c..7bfe505face19 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -11,9 +11,9 @@ import { arrayOfStringsSchema } from './common'; export const entityBaseSchema = z.object({ id: z.string(), type: z.string(), - identity_fields: arrayOfStringsSchema, + identity_fields: z.union([arrayOfStringsSchema, z.string()]), display_name: z.string(), - metrics: z.record(z.string(), z.number()), + metrics: z.optional(z.record(z.string(), z.number())), definition_version: z.string(), schema_version: z.string(), definition_id: z.string(), @@ -24,10 +24,13 @@ export interface MetadataRecord { } const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); + type Literal = z.infer; -type Metadata = Literal | { [key: string]: Metadata } | Metadata[]; +interface Metadata { + [key: string]: Metadata | Literal | Literal[]; +} export const entityMetadataSchema: z.ZodType = z.lazy(() => - z.union([literalSchema, z.array(entityMetadataSchema), z.record(entityMetadataSchema)]) + z.record(z.string(), z.union([literalSchema, z.array(literalSchema), entityMetadataSchema])) ); export const entityLatestSchema = z @@ -39,3 +42,6 @@ export const entityLatestSchema = z ), }) .and(entityMetadataSchema); + +export type EntityInstance = z.infer; +export type EntityMetadata = z.infer; diff --git a/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx b/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx index 8f42d755144a8..c8792ab3f9d9e 100644 --- a/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx +++ b/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx @@ -18,6 +18,7 @@ import { EuiFlexItem, EuiSuperDatePicker, type EuiSuperDatePickerProps, + EuiToolTip, } from '@elastic/eui'; import type { TimeRange } from '@kbn/es-query'; @@ -103,6 +104,10 @@ interface DatePickerWrapperProps { * when EuiSuperDatePicker's 'Refresh'|'Update' button is clicked */ onRefresh?: () => void; + /** + * Tooltip message for the update button + */ + tooltipMessage?: string; } /** @@ -122,6 +127,7 @@ export const DatePickerWrapper: FC = (props) => { isDisabled = false, needsUpdate, onRefresh, + tooltipMessage, } = props; const { data, @@ -318,34 +324,40 @@ export const DatePickerWrapper: FC = (props) => { recentlyUsedRanges={recentlyUsedRanges} dateFormat={dateFormat} commonlyUsedRanges={commonlyUsedRanges} + isDisabled={isDisabled} updateButtonProps={{ iconOnly: isWithinLBreakpoint, fill: false, ...(needsUpdate ? { needsUpdate } : {}), }} width={width} - isDisabled={isDisabled} /> {showRefresh === true || !isTimeRangeSelectorEnabled ? ( - - {needsUpdate ? ( - - ) : ( - - )} - + + + {needsUpdate ? ( + + ) : ( + + )} + + ) : null} 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 index 8e0fc6ad09479..bb1d5a2e2410e 100644 --- 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 @@ -221,4 +221,50 @@ describe('joinByKey', () => { }, }); }); + + it('deeply merges by unflatten keys', () => { + const joined = joinByKey( + [ + { + service: { + name: 'opbeans-node', + metrics: { + cpu: 0.1, + }, + }, + properties: { + foo: 'bar', + }, + }, + { + service: { + environment: 'prod', + metrics: { + memory: 0.5, + }, + }, + properties: { + foo: 'bar', + }, + }, + ], + 'properties.foo' + ); + + expect(joined).toEqual([ + { + service: { + name: 'opbeans-node', + environment: 'prod', + metrics: { + cpu: 0.1, + memory: 0.5, + }, + }, + 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 index 54e8ecdaf409b..93ec4261d04dc 100644 --- a/x-pack/packages/observability/observability_utils/array/join_by_key.ts +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -18,18 +18,29 @@ export type JoinedReturnType< } >; -type ArrayOrSingle = T | T[]; +function getValueByPath(obj: any, path: string): any { + return path.split('.').reduce((acc, keyPart) => { + // Check if acc is a valid object and has the key + return acc && Object.prototype.hasOwnProperty.call(acc, keyPart) ? acc[keyPart] : undefined; + }, obj); +} +type NestedKeys = T extends object + ? { [K in keyof T]: K extends string ? `${K}` | `${K}.${NestedKeys}` : never }[keyof T] + : never; + +type ArrayOrSingle = T | T[]; +type CombinedNestedKeys = (NestedKeys & NestedKeys) | (keyof T & keyof U); export function joinByKey< T extends Record, U extends UnionToIntersection, - V extends ArrayOrSingle + V extends ArrayOrSingle> >(items: T[], key: V): JoinedReturnType; export function joinByKey< T extends Record, U extends UnionToIntersection, - V extends ArrayOrSingle, + V extends ArrayOrSingle>, W extends JoinedReturnType, X extends (a: T, b: T) => ValuesType >(items: T[], key: V, mergeFn: X): W; @@ -45,7 +56,7 @@ export function joinByKey( 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])); + const stableKey = stableStringify(keys.map((k) => current[k] ?? getValueByPath(current, k))); if (map.has(stableKey)) { const item = map.get(stableKey); diff --git a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts index 0011e0f17c1c0..09013dcd5a506 100644 --- a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts +++ b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts @@ -9,6 +9,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import type { ESQLSearchResponse, ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; import { withSpan } from '@kbn/apm-utils'; import type { EsqlQueryRequest } from '@elastic/elasticsearch/lib/api/types'; +import { esqlResultToPlainObjects } from '../utils/esql_result_to_plain_objects'; type SearchRequest = ESSearchRequest & { index: string | string[]; @@ -16,6 +17,20 @@ type SearchRequest = ESSearchRequest & { size: number | boolean; }; +type EsqlQueryParameters = EsqlQueryRequest & { parseOutput?: boolean }; +type EsqlOutputParameters = Omit & { + parseOutput?: true; + format?: 'json'; + columnar?: false; +}; + +type EsqlParameters = EsqlOutputParameters | EsqlQueryParameters; + +export type InferEsqlResponseOf< + TOutput = unknown, + TParameters extends EsqlParameters = EsqlParameters +> = TParameters extends EsqlOutputParameters ? TOutput[] : ESQLSearchResponse; + /** * An Elasticsearch Client with a fully typed `search` method and built-in * APM instrumentation. @@ -25,7 +40,14 @@ export interface ObservabilityElasticsearchClient { operationName: string, parameters: TSearchRequest ): Promise>; - esql(operationName: string, parameters: EsqlQueryRequest): Promise; + esql( + operationName: string, + parameters: TQueryParams + ): Promise>; + esql( + operationName: string, + parameters: TQueryParams + ): Promise>; client: ElasticsearchClient; } @@ -40,11 +62,14 @@ export function createObservabilityEsClient({ }): ObservabilityElasticsearchClient { return { client, - esql(operationName: string, parameters: EsqlQueryRequest) { + esql( + operationName: string, + { parseOutput = true, format = 'json', columnar = false, ...parameters }: TSearchRequest + ) { logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`); return withSpan({ name: operationName, labels: { plugin } }, () => { return client.esql.query( - { ...parameters }, + { ...parameters, format, columnar }, { querystring: { drop_null_columns: true, @@ -54,7 +79,11 @@ export function createObservabilityEsClient({ }) .then((response) => { logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`); - return response as unknown as ESQLSearchResponse; + + const esqlResponse = response as unknown as ESQLSearchResponse; + + const shouldParseOutput = parseOutput && !columnar && format === 'json'; + return shouldParseOutput ? esqlResultToPlainObjects(esqlResponse) : esqlResponse; }) .catch((error) => { throw error; diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts index 96049f75ef156..717983a2958c5 100644 --- a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts +++ b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts @@ -6,25 +6,28 @@ */ import type { ESQLSearchResponse } from '@kbn/es-types'; +import { unflattenObject } from '../../object/unflatten_object'; -export function esqlResultToPlainObjects>( +export function esqlResultToPlainObjects( result: ESQLSearchResponse -): T[] { +): TDocument[] { return result.values.map((row) => { - return row.reduce>((acc, value, index) => { - const column = result.columns[index]; + return unflattenObject( + row.reduce>((acc, value, index) => { + const column = result.columns[index]; - if (!column) { - return acc; - } + if (!column) { + return acc; + } - // Removes the type suffix from the column name - const name = column.name.replace(/\.(text|keyword)$/, ''); - if (!acc[name]) { - acc[name] = value; - } + // Removes the type suffix from the column name + const name = column.name.replace(/\.(text|keyword)$/, ''); + if (!acc[name]) { + acc[name] = value; + } - return acc; - }, {}); - }) as T[]; + return acc; + }, {}) + ) as TDocument; + }); } diff --git a/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts index dbf9505193ecf..f542933f13b0c 100644 --- a/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts @@ -107,6 +107,7 @@ export const getAssistantSubFeaturesMap = ( ): Map => { const assistantSubFeaturesList: Array<[AssistantSubFeatureId, SubFeatureConfig]> = [ [AssistantSubFeatureId.updateAnonymization, updateAnonymizationSubFeature], + [AssistantSubFeatureId.manageGlobalKnowledgeBase, manageGlobalKnowledgeBaseSubFeature], ]; // Use the following code to add feature based on feature flag @@ -114,13 +115,6 @@ export const getAssistantSubFeaturesMap = ( // assistantSubFeaturesList.push([AssistantSubFeatureId.featureId, featureSubFeature]); // } - if (experimentalFeatures.assistantKnowledgeBaseByDefault) { - assistantSubFeaturesList.push([ - AssistantSubFeatureId.manageGlobalKnowledgeBase, - manageGlobalKnowledgeBaseSubFeature, - ]); - } - const assistantSubFeaturesMap = new Map( assistantSubFeaturesList ); diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/connector_types.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/connector_types.yaml index 1db9e155f2eec..db6262f04c010 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/connector_types.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/connector_types.yaml @@ -4,7 +4,6 @@ description: The type of connector. For example, `.email`, `.index`, `.jira`, `. enum: - .bedrock - .gemini - - .inference - .cases-webhook - .d3security - .email diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx index ec1f6774b6b46..d92ce014d68fb 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx @@ -104,7 +104,7 @@ export const useViewInLogPatternAnalysisAction = (dataViewId?: string): TableIte return ( { error?: Error | string; } -export const DataVisualizerTable = ({ +const UnmemoizedDataVisualizerTable = ({ items, pageState, updatePageState, @@ -506,3 +506,7 @@ export const DataVisualizerTable = ({ ); }; + +export const DataVisualizerTable = React.memo( + UnmemoizedDataVisualizerTable +) as typeof UnmemoizedDataVisualizerTable; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx index f2ac83e1d4bfb..c6190c87bcae5 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx @@ -24,12 +24,12 @@ import { EuiPanel, EuiProgress, EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/common'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import { getOrCreateDataViewByIndexPattern } from '../../search_strategy/requests/get_data_view_by_index_pattern'; import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; -import type { FieldVisConfig } from '../../../common/components/stats_table/types'; import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer'; import { useDataVisualizerKibana } from '../../../kibana_context'; import type { GetAdditionalLinks } from '../../../common/components/results_links'; @@ -49,6 +49,7 @@ import type { import type { ESQLQuery } from '../../search_strategy/requests/esql_utils'; import { isESQLQuery } from '../../search_strategy/requests/esql_utils'; import { FieldStatsComponentType } from '../../constants/field_stats_component_type'; +import { getReasonIfFieldStatsUnavailableForQuery } from '../../utils/get_reason_fieldstats_unavailable_for_esql_query'; export interface IndexDataVisualizerESQLProps { getAdditionalLinks?: GetAdditionalLinks; @@ -64,6 +65,8 @@ export const IndexDataVisualizerESQL: FC = (dataVi const [query, setQuery] = useState(DEFAULT_ESQL_QUERY); const [currentDataView, setCurrentDataView] = useState(); + const unsupportedReasonForQuery = getReasonIfFieldStatsUnavailableForQuery(localQuery); + const toggleShowEmptyFields = () => { setDataVisualizerListState({ ...dataVisualizerListState, @@ -202,8 +205,11 @@ export const IndexDataVisualizerESQL: FC = (dataVi const onTextLangQuerySubmit = useCallback( async (q: AggregateQuery | undefined) => { if (isESQLQuery(q)) { - resetData(); - setQuery(q); + const isUnsupported = getReasonIfFieldStatsUnavailableForQuery(q) !== undefined; + if (!isUnsupported) { + resetData(); + setQuery(q); + } } }, [resetData] @@ -224,8 +230,19 @@ export const IndexDataVisualizerESQL: FC = (dataVi data-test-subj="dataViewTitleHeader" direction="row" alignItems="center" - css={{ padding: `${euiTheme.euiSizeS} 0`, marginRight: `${euiTheme.euiSize}` }} - /> + css={{ padding: 0, marginRight: 0 }} + > + {unsupportedReasonForQuery ? ( + + + + ) : null} +
{isWithinLargeBreakpoint ? : null} = (dataVi width="full" needsUpdate={queryNeedsUpdate} onRefresh={handleRefresh} - isDisabled={!hasValidTimeField} + isDisabled={unsupportedReasonForQuery !== undefined} + tooltipMessage={unsupportedReasonForQuery} /> @@ -276,6 +294,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi hideRunQueryText={false} isLoading={queryHistoryStatus ?? false} displayDocumentationAsFlyout + disableSubmitAction={unsupportedReasonForQuery !== undefined} /> @@ -312,7 +331,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi - + void; onQuerySubmit: (query: AggregateQuery, abortController?: AbortController) => Promise; + disableSubmitAction?: boolean; } export const FieldStatsESQLEditor = ({ canEditTextBasedQuery = true, query, setQuery, onQuerySubmit, + disableSubmitAction = false, }: FieldStatsESQLEditorProps) => { const prevQuery = useRef(query); const [isVisualizationLoading, setIsVisualizationLoading] = useState(false); @@ -48,8 +50,8 @@ export const FieldStatsESQLEditor = ({ editorIsInline hideRunQueryText onTextLangQuerySubmit={onTextLangQuerySubmit} - isDisabled={false} - allowQueryCancellation + allowQueryCancellation={false} + disableSubmitAction={disableSubmitAction} isLoading={isVisualizationLoading} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx index 0e678e328894d..eb829e9a20cd8 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx @@ -38,6 +38,7 @@ import type { import { FieldStatsInitializerViewType } from '../grid_embeddable/types'; import { isESQLQuery } from '../../search_strategy/requests/esql_utils'; import { DataSourceTypeSelector } from './field_stats_initializer_view_type'; +import { getReasonIfFieldStatsUnavailableForQuery } from '../../utils/get_reason_fieldstats_unavailable_for_esql_query'; export interface FieldStatsInitializerProps { initialInput?: Partial; @@ -94,6 +95,12 @@ export const FieldStatisticsInitializer: FC = ({ }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataViewId, viewType, esqlQuery.esql, isEsqlMode]); + + const unsupportedReason = useMemo( + () => getReasonIfFieldStatsUnavailableForQuery(esqlQuery), + [esqlQuery] + ); + const onESQLQuerySubmit = useCallback( async (query: AggregateQuery, abortController?: AbortController) => { const adhocDataView = await getESQLAdHocDataview(query.esql, dataViews); @@ -101,11 +108,14 @@ export const FieldStatisticsInitializer: FC = ({ setDataViewId(adhocDataView.id); } - await onPreview({ - viewType, - dataViewId: adhocDataView?.id, - query, - }); + const supported = getReasonIfFieldStatsUnavailableForQuery(query) === undefined; + if (supported) { + await onPreview({ + viewType, + dataViewId: adhocDataView?.id, + query, + }); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [isEsqlMode] @@ -202,6 +212,7 @@ export const FieldStatisticsInitializer: FC = ({ } /> ) : null} + {initialInput?.viewType === FieldStatsInitializerViewType.ESQL && !isEsqlEnabled ? ( <> @@ -247,6 +258,7 @@ export const FieldStatisticsInitializer: FC = ({ query={esqlQuery} setQuery={setQuery} onQuerySubmit={onESQLQuerySubmit} + disableSubmitAction={!!unsupportedReason} /> ) : null} @@ -272,26 +284,39 @@ export const FieldStatisticsInitializer: FC = ({ /> - - - - - + + {unsupportedReason ? ( + + + + ) : null} + + + + + + + diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg.tsx new file mode 100644 index 0000000000000..9e49570ab6bfa --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +const FIELD_STATS_UNAVAILABLE_TITLE = i18n.translate( + 'xpack.dataVisualizer.fieldStats.unavailableTitle', + { + defaultMessage: 'Field statistics not supported for ES|QL queries', + } +); + +const FieldStatsUnavailableMessage = ({ + id, + title = FIELD_STATS_UNAVAILABLE_TITLE, +}: { + id?: string; + title?: string; +}) => { + return ( + + + + ); +}; + +// Default export for lazy loading +// eslint-disable-next-line import/no-default-export +export default FieldStatsUnavailableMessage; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx index 03dbb9c7af8c8..d991f02fb3958 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx @@ -16,11 +16,13 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker'; import type { DatePickerDependencies } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { pick } from 'lodash'; +import { isOfAggregateQueryType } from '@kbn/es-query'; import { getCoreStart, getPluginsStart } from '../../../../kibana_services'; import type { FieldStatisticTableEmbeddableProps, ESQLDataVisualizerGridEmbeddableState, } from './types'; +import FieldStatsUnavailableMessage from './embeddable_error_msg'; const EmbeddableESQLFieldStatsTableWrapper = dynamic( () => import('./embeddable_esql_field_stats_table') @@ -41,7 +43,10 @@ function isFieldStatisticTableEmbeddableState( const FieldStatisticsWrapperContent = (props: FieldStatisticTableEmbeddableProps) => { if (isESQLFieldStatisticTableEmbeddableState(props)) { - return ( + const isEsql = props.esqlQuery && isOfAggregateQueryType(props.esqlQuery); + return isEsql ? ( + + ) : ( => { - const { apiIsPresentationContainer } = await import('@kbn/presentation-containers'); - // we cannot have an async type check, so return the casted parentApi rather than a boolean - return apiIsPresentationContainer(parentApi) ? (parentApi as PresentationContainer) : undefined; -}; - -interface FieldStatsActionContext extends EmbeddableApiContext { - embeddable: FieldStatisticsTableEmbeddableApi; -} - -async function updatePanelFromFlyoutEdits({ - api, - isNewPanel, - deletePanel, - coreStart, - pluginStart, - initialState, -}: { - api: FieldStatisticsTableEmbeddableApi; - isNewPanel: boolean; - deletePanel?: () => void; - coreStart: CoreStart; - pluginStart: DataVisualizerStartDependencies; - initialState: FieldStatsInitialState; - fieldStatsControlsApi?: FieldStatsControlsApi; -}) { - const [ - { getOrCreateDataViewByIndexPattern }, - { FieldStatisticsInitializer }, - { tracksOverlays }, - { toMountPoint }, - { KibanaContextProvider }, - { isDefined }, - ] = await Promise.all([ - import('../search_strategy/requests/get_data_view_by_index_pattern'), - import('../embeddables/field_stats/field_stats_initializer'), - import('@kbn/presentation-containers'), - import('@kbn/react-kibana-mount'), - import('@kbn/kibana-react-plugin/public'), - import('@kbn/ml-is-defined'), - ]); - const parentApi = api.parentApi; - const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined; - const services = { - ...coreStart, - ...pluginStart, - }; - let hasChanged = false; - const cancelChanges = () => { - // Reset to initialState in case user has changed the preview state - if (hasChanged && api && initialState) { - api.updateUserInput(initialState); - } - - if (isNewPanel && deletePanel) { - deletePanel(); - } - flyoutSession.close(); - overlayTracker?.clearOverlays(); - }; - - const update = async (nextUpdate: FieldStatsInitialState) => { - const esqlQuery = nextUpdate?.query?.esql; - if (isDefined(esqlQuery)) { - const dv = await getOrCreateDataViewByIndexPattern( - pluginStart.data.dataViews, - esqlQuery, - undefined - ); - if (dv?.id && nextUpdate.dataViewId !== dv.id) { - nextUpdate.dataViewId = dv.id; - } - } - if (api) { - api.updateUserInput(nextUpdate); - } - - flyoutSession.close(); - overlayTracker?.clearOverlays(); - }; - const flyoutSession = services.overlays.openFlyout( - toMountPoint( - - { - if (api.updateUserInput) { - api.updateUserInput(nextUpdate); - hasChanged = true; - } - }} - onCreate={update} - onCancel={cancelChanges} - isNewPanel={isNewPanel} - /> - , - coreStart - ), - { - ownFocus: true, - size: 's', - paddingSize: 'm', - hideCloseButton: true, - type: 'push', - 'data-test-subj': 'fieldStatisticsInitializerFlyout', - onClose: cancelChanges, - } - ); - overlayTracker?.openOverlay(flyoutSession, { focusedPanelId: api.uuid }); -} - -export function createAddFieldStatsTableAction( - coreStart: CoreStart, - pluginStart: DataVisualizerStartDependencies -): UiActionsActionDefinition { - return { - id: 'create-field-stats-table', - grouping: COMMON_VISUALIZATION_GROUPING, - order: 10, - getIconType: () => 'fieldStatistics', - getDisplayName: () => - i18n.translate('xpack.dataVisualizer.fieldStatistics.displayName', { - defaultMessage: 'Field statistics', - }), - disabled: !coreStart.uiSettings.get(ENABLE_ESQL), - async isCompatible(context: EmbeddableApiContext) { - return ( - Boolean(await parentApiIsCompatible(context.embeddable)) && - coreStart.uiSettings.get(ENABLE_ESQL) - ); - }, - async execute(context) { - const [ - { IncompatibleActionError }, - { FIELD_STATS_EMBEDDABLE_TYPE }, - { FieldStatsInitializerViewType }, - ] = await Promise.all([ - import('@kbn/ui-actions-plugin/public'), - import('../embeddables/field_stats/constants'), - import('../embeddables/grid_embeddable/types'), - ]); - - const presentationContainerParent = await parentApiIsCompatible(context.embeddable); - if (!presentationContainerParent) throw new IncompatibleActionError(); - - const isEsqlEnabled = coreStart.uiSettings.get(ENABLE_ESQL); - try { - const defaultIndexPattern = await pluginStart.data.dataViews.getDefault(); - const defaultInitialState: FieldStatsInitialState = isEsqlEnabled - ? { - viewType: FieldStatsInitializerViewType.ESQL, - query: { - // Initial default query - esql: `from ${defaultIndexPattern?.getIndexPattern()} | limit 10`, - }, - } - : { - viewType: FieldStatsInitializerViewType.DATA_VIEW, - }; - const embeddable = await presentationContainerParent.addNewPanel< - object, - FieldStatisticsTableEmbeddableApi - >({ - panelType: FIELD_STATS_EMBEDDABLE_TYPE, - initialState: defaultInitialState, - }); - // open the flyout if embeddable has been created successfully - if (embeddable) { - const deletePanel = () => { - presentationContainerParent.removePanel(embeddable.uuid); - }; - - updatePanelFromFlyoutEdits({ - api: embeddable, - isNewPanel: true, - deletePanel, - coreStart, - pluginStart, - initialState: defaultInitialState, - }); - } - } catch (e) { - return Promise.reject(e); - } - }, - }; -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts deleted file mode 100644 index ac2f73860e4fb..0000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts +++ /dev/null @@ -1,20 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { CoreStart } from '@kbn/core-lifecycle-browser'; -import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public'; -import type { DataVisualizerStartDependencies } from '../../common/types/data_visualizer_plugin'; -import { createAddFieldStatsTableAction } from './create_field_stats_table'; - -export function registerDataVisualizerUiActions( - uiActions: UiActionsSetup, - coreStart: CoreStart, - pluginStart: DataVisualizerStartDependencies -) { - const addFieldStatsAction = createAddFieldStatsTableAction(coreStart, pluginStart); - uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addFieldStatsAction); -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_reason_fieldstats_unavailable_for_esql_query.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_reason_fieldstats_unavailable_for_esql_query.ts new file mode 100644 index 0000000000000..f6a9996aed42e --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_reason_fieldstats_unavailable_for_esql_query.ts @@ -0,0 +1,22 @@ +/* + * 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 { AggregateQuery } from '@kbn/es-query'; + +import type { Query } from '@kbn/es-query'; +import { queryCannotBeSampled } from '@kbn/esql-utils'; +import { i18n } from '@kbn/i18n'; + +export const getReasonIfFieldStatsUnavailableForQuery = ( + query?: AggregateQuery | Query | { [key: string]: any } +): string | undefined => { + if (queryCannotBeSampled(query)) { + return i18n.translate('xpack.dataVisualizer.fieldStats.unavailableForESQLQueryDescription', { + defaultMessage: `Field statistics are not available for ES|QL queries with 'MATCH' or 'QSTR' functions.`, + }); + } +}; diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index deaf4bdc99678..282ec23e3c303 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -21,7 +21,6 @@ import type { DataVisualizerStartDependencies, } from './application/common/types/data_visualizer_plugin'; import { registerEmbeddables } from './application/index_data_visualizer/embeddables/field_stats'; -import { registerDataVisualizerUiActions } from './application/index_data_visualizer/ui_actions'; export type DataVisualizerPluginSetup = ReturnType; export type DataVisualizerPluginStart = ReturnType; @@ -57,12 +56,6 @@ export class DataVisualizerPlugin registerEmbeddables(plugins.embeddable, core); } - const [coreStart, pluginStart] = await core.getStartServices(); - - if (plugins.uiActions) { - registerDataVisualizerUiActions(plugins.uiActions, coreStart, pluginStart); - } - if (plugins.home) { registerHomeAddData(plugins.home, this.resultsLinks); registerHomeFeatureCatalogue(plugins.home); @@ -84,6 +77,12 @@ export class DataVisualizerPlugin getIndexDataVisualizerComponent, getDataDriftComponent, getMaxBytesFormatted, + FieldStatsUnavailableMessage: dynamic( + async () => + import( + './application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg' + ) + ), FieldStatisticsTable: dynamic( async () => import( diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 7df4f57dc987b..970526cdf464e 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -86,7 +86,6 @@ "@kbn/core-lifecycle-browser", "@kbn/presentation-containers", "@kbn/react-kibana-mount", - "@kbn/visualizations-plugin", "@kbn/core-ui-settings-browser" ], "exclude": [ diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/msearch_query.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/msearch_query.ts deleted file mode 100644 index ae5adcfab61aa..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/msearch_query.ts +++ /dev/null @@ -1,71 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { MsearchQueryBody } from '../lib/langchain/elasticsearch_store/helpers/get_msearch_query_body'; - -/** - * This mock Elasticsearch msearch request body contains two queries: - * - The first query is a similarity (vector) search - * - The second query is a required KB document (terms) search - */ -export const mSearchQueryBody: MsearchQueryBody = { - body: [ - { - index: '.kibana-elastic-ai-assistant-kb', - }, - { - query: { - bool: { - must_not: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], - must: [ - { - semantic: { - field: 'semantic_text', - query: - 'Generate an ESQL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.', - }, - }, - ], - }, - }, - size: 1, - }, - { - index: '.kibana-elastic-ai-assistant-kb', - }, - { - query: { - bool: { - must: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], - }, - }, - size: 1, - }, - ], -}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/msearch_response.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/msearch_response.ts deleted file mode 100644 index 63439d5c07700..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/msearch_response.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { MsearchResponse } from '@elastic/elasticsearch/lib/api/types'; - -/** - * This mock response from an Elasticsearch msearch contains two hits, where - * the first hit is from a similarity (vector) search, and the second hit is a - * required KB document (terms) search. - */ -export const mockMsearchResponse: MsearchResponse = { - took: 142, - responses: [ - { - took: 142, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 129, - relation: 'eq', - }, - max_score: 21.658352, - hits: [ - { - _index: '.kibana-elastic-ai-assistant-kb', - _id: 'fa1c8ba1-25c9-4404-9736-09b7eb7124f8', - _score: 21.658352, - _ignored: ['text.keyword'], - _source: { - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc', - }, - vector: { - tokens: { - wild: 1.2001507, - // truncated for mock - }, - model_id: '.elser_model_2', - }, - text: "[[esql-from]]\n=== `FROM`\n\nThe `FROM` source command returns a table with up to 10,000 documents from a\ndata stream, index, or alias. Each row in the resulting table represents a\ndocument. Each column corresponds to a field, and can be accessed by the name\nof that field.\n\n[source,esql]\n----\nFROM employees\n----\n\nYou can use <> to refer to indices, aliases\nand data streams. This can be useful for time series data, for example to access\ntoday's index:\n\n[source,esql]\n----\nFROM \n----\n\nUse comma-separated lists or wildcards to query multiple data streams, indices,\nor aliases:\n\n[source,esql]\n----\nFROM employees-00001,employees-*\n----\n", - }, - }, - ], - }, - status: 200, - }, - { - took: 3, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 14, - relation: 'eq', - }, - max_score: 0.034783483, - hits: [ - { - _index: '.kibana-elastic-ai-assistant-kb', - _id: '280d4882-0f64-4471-a268-669a3f8c958f', - _score: 0.034783483, - _ignored: ['text.keyword'], - _source: { - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/example_queries/esql_example_query_0001.asciidoc', - required: true, - kbResource: 'esql', - }, - vector: { - tokens: { - user: 1.1084619, - // truncated for mock - }, - model_id: '.elser_model_2', - }, - text: '[[esql-example-queries]]\n\nThe following is an example an ES|QL query:\n\n```\nFROM logs-*\n| WHERE NOT CIDR_MATCH(destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")\n| STATS destcount = COUNT(destination.ip) by user.name, host.name\n| ENRICH ldap_lookup_new ON user.name\n| WHERE group.name IS NOT NULL\n| EVAL follow_up = CASE(\n destcount >= 100, "true",\n "false")\n| SORT destcount desc\n| KEEP destcount, host.name, user.name, group.name, follow_up\n```\n', - }, - }, - ], - }, - status: 200, - }, - ], -}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/raw_attack_discoveries.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/raw_attack_discoveries.ts deleted file mode 100644 index 1c43f112da2bb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/raw_attack_discoveries.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * A mock response from invoking the `attack-discovery` tool. - * This is a JSON string that represents the response from the tool - */ -export const getRawAttackDiscoveriesMock = () => - '{\n "alertsContextCount": 20,\n "attackDiscoveries": [\n {\n "alertIds": [\n "9bb601522d0c0b83783488a27a3ede5bd6a788f4f1ceef07cc8f12ac55f27563",\n "b9d6df8ab34e36c6868c097ff28dd01075df85a5ac1f084ef569ee8c6a4cf660",\n "014b433c3436ef5325cadacc35b6cb2ba8932a9c2ea0ba26d899f95c6fb61395",\n "28017987e64abb6ac486f1410f977d97ebd3a7172189cfdf943a48a59b968066"\n ],\n "detailsMarkdown": "- {{ host.name cb186c4a-3d70-4878-8ffe-18d84b5df86f }} (macOS {{ host.os.version 13.4 }}) executed a suspicious process {{ process.name unix1 }} with command line {{ process.command_line /Users/james/unix1 /Users/james/library/Keychains/login.keychain-db TempTemp1234!! }}\\\\n- The process was spawned by another suspicious process {{ process.parent.name My Go Application.app }} with command line {{ process.parent.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}\\\\n- The parent process was launched by the system process {{ process.parent.parent.name launchd }}\\\\n- Both the child and parent processes had untrusted code signatures\\\\n- The child process attempted to access the user\'s login keychain, potentially indicating credential theft",\n "entitySummaryMarkdown": "Suspicious activity on {{ host.name cb186c4a-3d70-4878-8ffe-18d84b5df86f }} by {{ user.name 3c8c81bd-0e52-4ce7-a836-48e718dfb6e4 }}",\n "mitreAttackTactics": [\n "Credential Access",\n "Defense Evasion",\n "Execution"\n ],\n "summaryMarkdown": "Suspicious activity detected on a macOS host involving a potentially malicious process attempting to access user credentials. The process was spawned by another untrusted process launched by the system, indicating a multi-stage attack potentially involving credential theft and defense evasion techniques.",\n "title": "Potential Credential Theft on macOS Host"\n },\n {\n "alertIds": [\n "64bcd8a322e6e6aebaee252982d0249cc96bdd75023ea05f58c228a7417c0dfc"\n ],\n "detailsMarkdown": "- {{ host.name cb186c4a-3d70-4878-8ffe-18d84b5df86f }} (macOS {{ host.os.version 13.4 }}) executed the system utility {{ process.name osascript }} with command line {{ process.command_line osascript -e display dialog \\"MacOS wants to access System Preferences\\\\n\\\\t\\\\t\\\\nPlease enter your password.\\" with title \\"System Preferences\\" with icon file \\"System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns\\" default answer \\"\\" giving up after 30 with hidden answer ¬ }}\\\\n- This appears to be an attempt to phish for user credentials by displaying a fake system dialog\\\\n- The osascript process was spawned by the suspicious process {{ process.parent.name My Go Application.app }} with untrusted code signature",\n "entitySummaryMarkdown": "Potential credential phishing attempt on {{ host.name cb186c4a-3d70-4878-8ffe-18d84b5df86f }} targeting {{ user.name 3c8c81bd-0e52-4ce7-a836-48e718dfb6e4 }}",\n "mitreAttackTactics": [\n "Credential Access",\n "Initial Access",\n "Execution"\n ],\n "summaryMarkdown": "A credential phishing attempt was detected on a macOS host, likely initiated by a malicious process. The attack used osascript to display a fake system dialog prompting the user to enter their password.",\n "title": "Credential Phishing Attempt on macOS"\n },\n {\n "alertIds": [\n "245b60b908ddd84cad06671e273aa7be50699abd27e59423be4415f38c4aeb99",\n "616ac711e967e07a9b725e66aa93321eabf29e4b51f9598a4a11f21ab7ed0f12",\n "035c0295b1c64fd2ebba1b751a3565fd6759942247e9df6e1496c5e332d51840"\n ],\n "detailsMarkdown": "- {{ host.name cb186c4a-3d70-4878-8ffe-18d84b5df86f }} (macOS {{ host.os.version 13.4 }}) executed a suspicious process {{ process.name My Go Application.app }} with command line {{ process.command_line xpcproxy application.Appify by Machine Box.My Go Application.20.23 }}\\\\n- This process had an untrusted code signature and was launched by the system process {{ process.parent.name launchd }}\\\\n- It appears to have spawned the process {{ process.name unix1 }} in an attempt to obfuscate its activities\\\\n- The unix1 process attempted to make itself executable by running {{ process.name chmod }} with arguments {{ process.command_line chmod 777 /Users/james/unix1 }}",\n "entitySummaryMarkdown": "Suspicious activity involving process obfuscation on {{ host.name cb186c4a-3d70-4878-8ffe-18d84b5df86f }} by {{ user.name fec12d87-2476-4b82-a50d-0829f3815a42 }}",\n "mitreAttackTactics": [\n "Defense Evasion",\n "Execution"\n ],\n "summaryMarkdown": "A suspicious process was detected on a macOS host that appeared to be attempting to obfuscate its activities by spawning other processes and making them executable. The initial process had an untrusted code signature, indicating potentially malicious intent.",\n "title": "Process Obfuscation on macOS Host"\n },\n {\n "alertIds": [\n "54901fb5b0ed88f0c8d737613868a3d62ebc541d31b757349bbe7999d868ce48"\n ],\n "detailsMarkdown": "- {{ host.name 23166d28-d6da-4801-b701-d21ce1a489e5 }} (Windows {{ host.os.version 21H2 (10.0.20348.1607) }}) created a suspicious script file {{ file.path C:\\\\ProgramData\\\\WindowsAppPool\\\\AppPool.vbs }}\\\\n- The file was created by a Microsoft Word process ({{ process.name WINWORD.EXE }}) with trusted code signature\\\\n- This may indicate an attempt to establish persistence or command-and-control through scripting",\n "entitySummaryMarkdown": "Suspicious script file created on {{ host.name 23166d28-d6da-4801-b701-d21ce1a489e5 }} by {{ user.name 45bec1b8-eb98-4ddc-aafb-e3f7e02236dc }}",\n "mitreAttackTactics": [\n "Command and Control",\n "Execution"\n ],\n "summaryMarkdown": "A suspicious VBScript file was created on a Windows host, potentially by an compromised Microsoft Word process. This may be an attempt to establish persistence or command-and-control capabilities through scripting.",\n "title": "Suspicious Script File Creation on Windows"\n },\n {\n "alertIds": [\n "7fe0025f2d2b0d32f04b0e533466666967a21a98adae7499cb05add3355b48fc",\n "3875cbad10604636b892d15f7ff753a02a37d3e4bbe91a39a0fcf72f89101e31",\n "bb2767ebef06a5dc2511e2b865f5ed012dfdf20081bc33cab5c9f20b99e01d8f",\n "76d99c72442819a019dfbf3936cda9a6c5713d84a9ae685b2c4e0bb55e5b9862",\n "0f985965cb3d3b14007873290b9fc8f26f1b6ca0945499dfb693787ea6569265"\n ],\n "detailsMarkdown": "- {{ host.name 9a0ea998-7ce5-4dbb-a690-9856eca617ac }} (Windows {{ host.os.version 21H2 (10.0.20348.1607) }}) executed a suspicious PowerShell script {{ process.command_line \\"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\\" -exec bypass -file C:\\\\ProgramData\\\\WindowsAppPool\\\\AppPool.ps1 }}\\\\n- The script was launched by the wscript process, which was spawned by a Microsoft Word process ({{ process.parent.name WINWORD.EXE }})\\\\n- The Word process also created a scheduled task to periodically execute the script\\\\n- The PowerShell script appears to be obfuscated, potentially to hide malicious activities\\\\n- This chain of events indicates a multi-stage attack potentially initiated by a malicious Office document",\n "entitySummaryMarkdown": "Suspicious PowerShell activity on {{ host.name 9a0ea998-7ce5-4dbb-a690-9856eca617ac }} by {{ user.name 45bec1b8-eb98-4ddc-aafb-e3f7e02236dc }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Defense Evasion"\n ],\n "summaryMarkdown": "A multi-stage attack was detected on a Windows host, potentially initiated by a malicious Microsoft Office document. The attack involved creating a scheduled task to execute an obfuscated PowerShell script, likely to hide malicious activities. This indicates techniques for initial access, execution, and defense evasion.",\n "title": "Multi-Stage Attack on Windows Host"\n },\n {\n "alertIds": [\n "a0c49fb228eca1685bd41df0ab66ca1977140de7916663e7a0918087220dd402",\n "a252ca3096831e3eeab07ab70e9269f98b5a66617b44d709425898813326ca63",\n "0ff7d411ca25a5b851e43562c9c660062624498f908ff4b63590d4b5304682af",\n "4d612c721e432598a5b7ea7bbeb2aaa2944c0a35e263d9984297b5416530c88f"\n ],\n "detailsMarkdown": "- {{ host.name 634eb7d8-0ce0-4591-b5f5-fb65803b89d8 }} (Windows {{ host.os.version 21H2 (10.0.20348.1607) }}) executed a suspicious PowerShell script {{ process.command_line \\"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\\" -ep bypass -file \\"C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\Package Installation Dir\\\\chch.ps1\\" }}\\\\n- The script was launched by the msiexec.exe process, which may indicate an attempt to use a trusted Windows utility for defense evasion\\\\n- Elastic Endpoint detected the Bb malware family in the PowerShell process memory\\\\n- The PowerShell process also made network connections, potentially for command-and-control or data exfiltration",\n "entitySummaryMarkdown": "Malware detected on {{ host.name 634eb7d8-0ce0-4591-b5f5-fb65803b89d8 }} targeting {{ user.name 45bec1b8-eb98-4ddc-aafb-e3f7e02236dc }}",\n "mitreAttackTactics": [\n "Defense Evasion",\n "Execution"\n ],\n "summaryMarkdown": "The B malware was detected on a Windows host, executed through a PowerShell script launched by the msiexec.exe process. This appears to be an attempt to use a trusted Windows utility for defense evasion. The malware process also made network connections, potentially for command-and-control or data exfiltration.",\n "title": "Bb Malware Execution on Windows"\n },\n {\n "alertIds": [\n "764c0944288db1704f7a0fff2db7fe19e8285fa4272dec828ae4186ba0dfd3b3",\n "85672064aeb762a1121139a6d98fd3c5f6be8f18b49e4504c3f5e5a36679afe7"\n ],\n "detailsMarkdown": "- {{ host.name d813c7ba-6141-4292-8f40-c800c27645a4 }} (Linux {{ host.os.version 22.04.1 }}) executed a suspicious process {{ process.command_line sh -c /bin/rm -f /dev/shm/kdmtmpflush;/bin/cp ./74ef6cc38f5a1a80148752b63c117e6846984debd2af806c65887195a8eccc56 /dev/shm/kdmtmpflush && /bin/chmod 755 /dev/shm/kdmtmpflush && /dev/shm/kdmtmpflush --init && /bin/rm -f /dev/shm/kdmtmpflush }}\\\\n- This copied a file with SHA256 hash {{ file.hash.sha256 74ef6cc38f5a1a80148752b63c117e6846984debd2af806c65887195a8eccc56 }} to /dev/shm/kdmtmpflush, made it executable, and executed it\\\\n- Elastic Endpoint detected the Door malware family associated with this file",\n "entitySummaryMarkdown": "Malware executed on {{ host.name d813c7ba-6141-4292-8f40-c800c27645a4 }} by {{ user.name fec12d87-2476-4b82-a50d-0829f3815a42 }}",\n "mitreAttackTactics": [\n "Execution"\n ],\n "summaryMarkdown": "The Door malware was executed on a Linux host by copying an untrusted file to a temporary path, making it executable, and running it. This indicates malicious code execution on the compromised system.",\n "title": "Door Malware Execution on Linux"\n }\n ]\n}'; - -export const getRawAttackDiscoveriesReplacementsMock = () => ({ - '3c8c81bd-0e52-4ce7-a836-48e718dfb6e4': 'james', - 'cb186c4a-3d70-4878-8ffe-18d84b5df86f': 'SRVMAC08', - 'fec12d87-2476-4b82-a50d-0829f3815a42': 'root', - '45bec1b8-eb98-4ddc-aafb-e3f7e02236dc': 'Administrator', - '23166d28-d6da-4801-b701-d21ce1a489e5': 'SRVWIN07-PRIV', - '9a0ea998-7ce5-4dbb-a690-9856eca617ac': 'SRVWIN07', - '634eb7d8-0ce0-4591-b5f5-fb65803b89d8': 'SRVWIN06', - 'd813c7ba-6141-4292-8f40-c800c27645a4': 'SRVNIX05', -}); diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts index f6f3007c8f948..698645e8d3c55 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts @@ -67,13 +67,6 @@ export const getPostKnowledgeBaseRequest = (resource?: string) => query: { resource }, }); -export const getDeleteKnowledgeBaseRequest = (resource?: string) => - requestMock.create({ - method: 'delete', - path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL, - query: { resource }, - }); - export const getGetCapabilitiesRequest = () => requestMock.create({ method: 'get', diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts index d53ceaa586975..a065c7de42586 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts @@ -127,11 +127,11 @@ const createElasticAssistantRequestContextMock = ( () => clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient ) as unknown as jest.MockInstance< Promise, - [params: GetAIAssistantKnowledgeBaseDataClientParams], + [params?: GetAIAssistantKnowledgeBaseDataClientParams], unknown > & (( - params: GetAIAssistantKnowledgeBaseDataClientParams + params?: GetAIAssistantKnowledgeBaseDataClientParams ) => Promise), getCurrentUser: jest.fn(), getServerBasePath: jest.fn(), diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/terms.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/terms.ts deleted file mode 100644 index 0606c905d6df3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/terms.ts +++ /dev/null @@ -1,28 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Field, FieldValue, QueryDslTermQuery } from '@elastic/elasticsearch/lib/api/types'; - -/** - * These (mock) terms may be used in multiple queries. - * - * For example, it may be be used in a vector search to exclude the required `esql` KB docs. - * - * It may also be used in a terms search to find all of the required `esql` KB docs. - */ -export const mockTerms: Array>> = [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, -]; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/terms_search_query.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/terms_search_query.ts deleted file mode 100644 index c8af748516a1f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/terms_search_query.ts +++ /dev/null @@ -1,28 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; - -/** - * This Elasticsearch query DSL is a terms search for required `esql` KB docs - */ -export const mockTermsSearchQuery: QueryDslQueryContainer = { - bool: { - must: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], - }, -}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/vector_search_query.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/vector_search_query.ts deleted file mode 100644 index 04263c5d242bb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/vector_search_query.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; - -/** - * A mock vector search query DSL - */ -export const mockVectorSearchQuery: QueryDslQueryContainer = { - bool: { - must_not: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], - must: [ - { - semantic: { - field: 'semantic_text', - query: - 'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.', - }, - }, - ], - }, -} as QueryDslQueryContainer; diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts index 77a1e37df965f..8e1d749c7f78b 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts @@ -14,11 +14,9 @@ import { } from '@kbn/core/server'; import { - DocumentEntryCreateFields, KnowledgeBaseEntryCreateProps, KnowledgeBaseEntryResponse, KnowledgeBaseEntryUpdateProps, - Metadata, } from '@kbn/elastic-assistant-common'; import { CREATE_KNOWLEDGE_BASE_ENTRY_ERROR_EVENT, @@ -33,9 +31,8 @@ export interface CreateKnowledgeBaseEntryParams { logger: Logger; spaceId: string; user: AuthenticatedUser; - knowledgeBaseEntry: KnowledgeBaseEntryCreateProps | LegacyKnowledgeBaseEntryCreateProps; + knowledgeBaseEntry: KnowledgeBaseEntryCreateProps; global?: boolean; - isV2?: boolean; telemetry: AnalyticsServiceSetup; } @@ -47,25 +44,16 @@ export const createKnowledgeBaseEntry = async ({ knowledgeBaseEntry, logger, global = false, - isV2 = false, telemetry, }: CreateKnowledgeBaseEntryParams): Promise => { const createdAt = new Date().toISOString(); - const body = isV2 - ? transformToCreateSchema({ - createdAt, - spaceId, - user, - entry: knowledgeBaseEntry as unknown as KnowledgeBaseEntryCreateProps, - global, - }) - : transformToLegacyCreateSchema({ - createdAt, - spaceId, - user, - entry: knowledgeBaseEntry as unknown as TransformToLegacyCreateSchemaProps['entry'], - global, - }); + const body = transformToCreateSchema({ + createdAt, + spaceId, + user, + entry: knowledgeBaseEntry as unknown as KnowledgeBaseEntryCreateProps, + global, + }); const telemetryPayload = { entryType: body.type, required: body.required ?? false, @@ -156,13 +144,7 @@ export const transformToUpdateSchema = ({ }; }; -export const getUpdateScript = ({ - entry, - isPatch, -}: { - entry: UpdateKnowledgeBaseEntrySchema; - isPatch?: boolean; -}) => { +export const getUpdateScript = ({ entry }: { entry: UpdateKnowledgeBaseEntrySchema }) => { // Cannot use script for updating documents with semantic_text fields return { doc: { @@ -230,45 +212,3 @@ export const transformToCreateSchema = ({ semantic_text: entry.text, }; }; - -export type LegacyKnowledgeBaseEntryCreateProps = Omit< - DocumentEntryCreateFields, - 'kbResource' | 'source' -> & { - metadata: Metadata; -}; - -interface TransformToLegacyCreateSchemaProps { - createdAt: string; - spaceId: string; - user: AuthenticatedUser; - entry: LegacyKnowledgeBaseEntryCreateProps; - global?: boolean; -} - -export const transformToLegacyCreateSchema = ({ - createdAt, - spaceId, - user, - entry, - global = false, -}: TransformToLegacyCreateSchemaProps): CreateKnowledgeBaseEntrySchema => { - return { - '@timestamp': createdAt, - created_at: createdAt, - created_by: user.profile_uid ?? 'unknown', - updated_at: createdAt, - updated_by: user.profile_uid ?? 'unknown', - namespace: spaceId, - users: global - ? [] - : [ - { - id: user.profile_uid, - name: user.username, - }, - ], - ...entry, - vector: undefined, - }; -}; diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts index 348efb5a18f7d..1a075202cf3cd 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/field_maps_configuration.ts @@ -9,89 +9,6 @@ import { FieldMap } from '@kbn/data-stream-adapter'; export const ASSISTANT_ELSER_INFERENCE_ID = 'elastic-security-ai-assistant-elser2'; export const knowledgeBaseFieldMap: FieldMap = { - '@timestamp': { - type: 'date', - array: false, - required: false, - }, - id: { - type: 'keyword', - array: false, - required: true, - }, - created_at: { - type: 'date', - array: false, - required: false, - }, - created_by: { - type: 'keyword', - array: false, - required: false, - }, - updated_at: { - type: 'date', - array: false, - required: false, - }, - updated_by: { - type: 'keyword', - array: false, - required: false, - }, - users: { - type: 'nested', - array: true, - required: false, - }, - 'users.id': { - type: 'keyword', - array: false, - required: true, - }, - 'users.name': { - type: 'keyword', - array: false, - required: false, - }, - metadata: { - type: 'object', - array: false, - required: false, - }, - 'metadata.kbResource': { - type: 'keyword', - array: false, - required: false, - }, - 'metadata.required': { - type: 'boolean', - array: false, - required: false, - }, - 'metadata.source': { - type: 'keyword', - array: false, - required: false, - }, - text: { - type: 'text', - array: false, - required: true, - }, - vector: { - type: 'object', - array: false, - required: false, - }, - 'vector.tokens': { - type: 'rank_features', - array: false, - required: false, - }, -} as const; - -export const knowledgeBaseFieldMapV2: FieldMap = { // Base fields '@timestamp': { type: 'date', diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts index a19b3f0945086..88ecae26cf19f 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts @@ -27,37 +27,29 @@ export const isModelAlreadyExistsError = (error: Error) => { * * @param filter - Optional filter to apply to the search * @param kbResource - Specific resource tag to filter for, e.g. 'esql' or 'user' - * @param modelId - ID of the model to search with, e.g. `.elser_model_2` * @param query - The search query provided by the user * @param required - Whether to only include required entries * @param user - The authenticated user - * @param v2KnowledgeBaseEnabled whether the new v2 KB is enabled * @returns */ export const getKBVectorSearchQuery = ({ filter, kbResource, - modelId, query, required, user, - v2KnowledgeBaseEnabled = false, }: { filter?: QueryDslQueryContainer | undefined; kbResource?: string | undefined; - modelId: string; query?: string; required?: boolean | undefined; user: AuthenticatedUser; - v2KnowledgeBaseEnabled: boolean; }): QueryDslQueryContainer => { - const kbResourceKey = v2KnowledgeBaseEnabled ? 'kb_resource' : 'metadata.kbResource'; - const requiredKey = v2KnowledgeBaseEnabled ? 'required' : 'metadata.required'; const resourceFilter = kbResource ? [ { term: { - [kbResourceKey]: kbResource, + kb_resource: kbResource, }, }, ] @@ -66,7 +58,7 @@ export const getKBVectorSearchQuery = ({ ? [ { term: { - [requiredKey]: required, + required, }, }, ] @@ -120,7 +112,7 @@ export const getKBVectorSearchQuery = ({ text_expansion: { 'vector.tokens': { model_id: string; model_text: string } }; }> = []; - if (v2KnowledgeBaseEnabled && query) { + if (query) { semanticTextFilter = [ { semantic: { @@ -129,17 +121,6 @@ export const getKBVectorSearchQuery = ({ }, }, ]; - } else if (!v2KnowledgeBaseEnabled) { - semanticTextFilter = [ - { - text_expansion: { - 'vector.tokens': { - model_id: modelId, - model_text: query as string, - }, - }, - }, - ]; } return { diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index 50e124321fe6c..fae987b6d5083 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -29,20 +29,11 @@ import { AnalyticsServiceSetup, ElasticsearchClient } from '@kbn/core/server'; import { IndexPatternsFetcher } from '@kbn/data-views-plugin/server'; import { map } from 'lodash'; import { AIAssistantDataClient, AIAssistantDataClientParams } from '..'; -import { AssistantToolParams, GetElser } from '../../types'; -import { - createKnowledgeBaseEntry, - LegacyKnowledgeBaseEntryCreateProps, - transformToCreateSchema, - transformToLegacyCreateSchema, -} from './create_knowledge_base_entry'; +import { GetElser } from '../../types'; +import { createKnowledgeBaseEntry, transformToCreateSchema } from './create_knowledge_base_entry'; import { EsDocumentEntry, EsIndexEntry, EsKnowledgeBaseEntrySchema } from './types'; import { transformESSearchToKnowledgeBaseEntry } from './transforms'; -import { - ESQL_DOCS_LOADED_QUERY, - SECURITY_LABS_RESOURCE, - USER_RESOURCE, -} from '../../routes/knowledge_base/constants'; +import { SECURITY_LABS_RESOURCE, USER_RESOURCE } from '../../routes/knowledge_base/constants'; import { getKBVectorSearchQuery, getStructuredToolForIndexEntry, @@ -61,7 +52,6 @@ import { ASSISTANT_ELSER_INFERENCE_ID } from './field_maps_configuration'; */ export interface GetAIAssistantKnowledgeBaseDataClientParams { modelIdOverride?: string; - v2KnowledgeBaseEnabled?: boolean; manageGlobalKnowledgeBaseAIAssistant?: boolean; } @@ -71,7 +61,6 @@ interface KnowledgeBaseDataClientParams extends AIAssistantDataClientParams { getIsKBSetupInProgress: () => boolean; ingestPipelineResourceName: string; setIsKBSetupInProgress: (isInProgress: boolean) => void; - v2KnowledgeBaseEnabled: boolean; manageGlobalKnowledgeBaseAIAssistant: boolean; } export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { @@ -82,11 +71,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { public get isSetupInProgress() { return this.options.getIsKBSetupInProgress(); } - - public get isV2KnowledgeBaseEnabled() { - return this.options.v2KnowledgeBaseEnabled; - } - /** * Returns whether setup of the Knowledge Base can be performed (essentially an ML features check) * @@ -150,70 +134,39 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { }; /** - * Deploy the ELSER model with default configuration - */ - private deployModel = async () => { - const elserId = await this.options.getElserId(); - this.options.logger.debug(`Deploying ELSER model '${elserId}'...`); - try { - const esClient = await this.options.elasticsearchClientPromise; - await esClient.ml.startTrainedModelDeployment({ - model_id: elserId, - wait_for: 'fully_allocated', - }); - } catch (error) { - this.options.logger.error(`Error deploying ELSER model '${elserId}':\n${error}`); - throw new Error(`Error deploying ELSER model '${elserId}':\n${error}`); - } - }; - - /** - * Checks if the provided model is deployed and allocated in Elasticsearch + * Checks if the inference endpoint is deployed and allocated in Elasticsearch * * @returns Promise indicating whether the model is deployed */ - public isModelDeployed = async (): Promise => { - const elserId = await this.options.getElserId(); - this.options.logger.debug(`Checking if ELSER model '${elserId}' is deployed...`); - - try { - if (this.isV2KnowledgeBaseEnabled) { - return await this.isInferenceEndpointExists(); - } else { - const esClient = await this.options.elasticsearchClientPromise; - const getResponse = await esClient.ml.getTrainedModelsStats({ - model_id: elserId, - }); - - // For standardized way of checking deployment status see: https://github.com/elastic/elasticsearch/issues/106986 - const isReadyESS = (stats: MlTrainedModelStats) => - stats.deployment_stats?.state === 'started' && - stats.deployment_stats?.allocation_status.state === 'fully_allocated'; - - const isReadyServerless = (stats: MlTrainedModelStats) => - (stats.deployment_stats?.nodes as unknown as MlTrainedModelDeploymentNodesStats[])?.some( - (node) => node.routing_state.routing_state === 'started' - ); - - return getResponse.trained_model_stats?.some( - (stats) => isReadyESS(stats) || isReadyServerless(stats) - ); - } - } catch (e) { - this.options.logger.debug(`Error checking if ELSER model '${elserId}' is deployed: ${e}`); - // Returns 404 if it doesn't exist - return false; - } - }; - public isInferenceEndpointExists = async (): Promise => { try { const esClient = await this.options.elasticsearchClientPromise; - return !!(await esClient.inference.get({ + const inferenceExists = !!(await esClient.inference.get({ inference_id: ASSISTANT_ELSER_INFERENCE_ID, task_type: 'sparse_embedding', })); + if (!inferenceExists) { + return false; + } + const elserId = await this.options.getElserId(); + const getResponse = await esClient.ml.getTrainedModelsStats({ + model_id: elserId, + }); + + // For standardized way of checking deployment status see: https://github.com/elastic/elasticsearch/issues/106986 + const isReadyESS = (stats: MlTrainedModelStats) => + stats.deployment_stats?.state === 'started' && + stats.deployment_stats?.allocation_status.state === 'fully_allocated'; + + const isReadyServerless = (stats: MlTrainedModelStats) => + (stats.deployment_stats?.nodes as unknown as MlTrainedModelDeploymentNodesStats[])?.some( + (node) => node.routing_state.routing_state === 'started' + ); + + return getResponse.trained_model_stats?.some( + (stats) => isReadyESS(stats) || isReadyServerless(stats) + ); } catch (error) { this.options.logger.debug( `Error checking if Inference endpoint ${ASSISTANT_ELSER_INFERENCE_ID} exists: ${error}` @@ -227,25 +180,24 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { this.options.logger.debug(`Deploying ELSER model '${elserId}'...`); try { const esClient = await this.options.elasticsearchClientPromise; - if (this.isV2KnowledgeBaseEnabled) { - await esClient.inference.put({ - task_type: 'sparse_embedding', - inference_id: ASSISTANT_ELSER_INFERENCE_ID, - inference_config: { - service: 'elasticsearch', - service_settings: { - adaptive_allocations: { - enabled: true, - min_number_of_allocations: 0, - max_number_of_allocations: 8, - }, - num_threads: 1, - model_id: elserId, + + await esClient.inference.put({ + task_type: 'sparse_embedding', + inference_id: ASSISTANT_ELSER_INFERENCE_ID, + inference_config: { + service: 'elasticsearch', + service_settings: { + adaptive_allocations: { + enabled: true, + min_number_of_allocations: 0, + max_number_of_allocations: 8, }, - task_settings: {}, + num_threads: 1, + model_id: elserId, }, - }); - } + task_settings: {}, + }, + }); } catch (error) { this.options.logger.error( `Error creating inference endpoint for ELSER model '${elserId}':\n${error}` @@ -268,11 +220,9 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { */ public setupKnowledgeBase = async ({ soClient, - v2KnowledgeBaseEnabled = true, ignoreSecurityLabs = false, }: { soClient: SavedObjectsClientContract; - v2KnowledgeBaseEnabled?: boolean; ignoreSecurityLabs?: boolean; }): Promise => { if (this.options.getIsKBSetupInProgress()) { @@ -284,40 +234,38 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { this.options.setIsKBSetupInProgress(true); const elserId = await this.options.getElserId(); - if (v2KnowledgeBaseEnabled) { - // Delete legacy ESQL knowledge base docs if they exist, and silence the error if they do not - try { - const esClient = await this.options.elasticsearchClientPromise; - const legacyESQL = await esClient.deleteByQuery({ - index: this.indexTemplateAndPattern.alias, - query: { - bool: { - must: [{ terms: { 'metadata.kbResource': ['esql', 'unknown'] } }], - }, + // Delete legacy ESQL knowledge base docs if they exist, and silence the error if they do not + try { + const esClient = await this.options.elasticsearchClientPromise; + const legacyESQL = await esClient.deleteByQuery({ + index: this.indexTemplateAndPattern.alias, + query: { + bool: { + must: [{ terms: { 'metadata.kbResource': ['esql', 'unknown'] } }], }, - }); - if (legacyESQL?.total != null && legacyESQL?.total > 0) { - this.options.logger.info( - `Removed ${legacyESQL?.total} ESQL knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.` - ); - } - // Delete any existing Security Labs content - const securityLabsDocs = await esClient.deleteByQuery({ - index: this.indexTemplateAndPattern.alias, - query: { - bool: { - must: [{ terms: { kb_resource: [SECURITY_LABS_RESOURCE] } }], - }, + }, + }); + if (legacyESQL?.total != null && legacyESQL?.total > 0) { + this.options.logger.info( + `Removed ${legacyESQL?.total} ESQL knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.` + ); + } + // Delete any existing Security Labs content + const securityLabsDocs = await esClient.deleteByQuery({ + index: this.indexTemplateAndPattern.alias, + query: { + bool: { + must: [{ terms: { kb_resource: [SECURITY_LABS_RESOURCE] } }], }, - }); - if (securityLabsDocs?.total) { - this.options.logger.info( - `Removed ${securityLabsDocs?.total} Security Labs knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.` - ); - } - } catch (e) { - this.options.logger.info('No legacy ESQL or Security Labs knowledge base docs to delete'); + }, + }); + if (securityLabsDocs?.total) { + this.options.logger.info( + `Removed ${securityLabsDocs?.total} Security Labs knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.` + ); } + } catch (e) { + this.options.logger.info('No legacy ESQL or Security Labs knowledge base docs to delete'); } try { @@ -336,39 +284,22 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { this.options.logger.debug(`ELSER model '${elserId}' is already installed`); } - if (!this.isV2KnowledgeBaseEnabled) { - const isDeployed = await this.isModelDeployed(); - if (!isDeployed) { - await this.deployModel(); - await pRetry( - async () => - (await this.isModelDeployed()) - ? Promise.resolve() - : Promise.reject(new Error('Model not deployed')), - { minTimeout: 2000, retries: 10 } - ); - this.options.logger.debug(`ELSER model '${elserId}' successfully deployed!`); - } else { - this.options.logger.debug(`ELSER model '${elserId}' is already deployed`); - } - } else { - const inferenceExists = await this.isInferenceEndpointExists(); - if (!inferenceExists) { - await this.createInferenceEndpoint(); + const inferenceExists = await this.isInferenceEndpointExists(); + if (!inferenceExists) { + await this.createInferenceEndpoint(); - this.options.logger.debug( - `Inference endpoint for ELSER model '${elserId}' successfully deployed!` - ); - } else { - this.options.logger.debug( - `Inference endpoint for ELSER model '${elserId}' is already deployed` - ); - } + this.options.logger.debug( + `Inference endpoint for ELSER model '${elserId}' successfully deployed!` + ); + } else { + this.options.logger.debug( + `Inference endpoint for ELSER model '${elserId}' is already deployed` + ); } this.options.logger.debug(`Checking if Knowledge Base docs have been loaded...`); - if (v2KnowledgeBaseEnabled && !ignoreSecurityLabs) { + if (!ignoreSecurityLabs) { const labsDocsLoaded = await this.isSecurityLabsDocsLoaded(); if (!labsDocsLoaded) { this.options.logger.debug(`Loading Security Labs KB docs...`); @@ -415,39 +346,20 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { const { errors, docs_created: docsCreated } = await writer.bulk({ documentsToCreate: documents.map((doc) => { // v1 schema has metadata nested in a `metadata` object - if (this.options.v2KnowledgeBaseEnabled) { - return transformToCreateSchema({ - createdAt: changedAt, - spaceId: this.spaceId, - user: authenticatedUser, - entry: { - type: DocumentEntryType.value, - name: 'unknown', - text: doc.pageContent, - kbResource: doc.metadata.kbResource ?? 'unknown', - required: doc.metadata.required ?? false, - source: doc.metadata.source ?? 'unknown', - }, - global, - }); - } else { - return transformToLegacyCreateSchema({ - createdAt: changedAt, - spaceId: this.spaceId, - user: authenticatedUser, - entry: { - type: DocumentEntryType.value, - name: 'unknown', - text: doc.pageContent, - metadata: { - kbResource: doc.metadata.kbResource ?? 'unknown', - required: doc.metadata.required ?? false, - source: doc.metadata.source ?? 'unknown', - }, - }, - global, - }); - } + return transformToCreateSchema({ + createdAt: changedAt, + spaceId: this.spaceId, + user: authenticatedUser, + entry: { + type: DocumentEntryType.value, + name: 'unknown', + text: doc.pageContent, + kbResource: doc.metadata.kbResource ?? 'unknown', + required: doc.metadata.required ?? false, + source: doc.metadata.source ?? 'unknown', + }, + global, + }); }), authenticatedUser, }); @@ -467,18 +379,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { return created?.data ? transformESSearchToKnowledgeBaseEntry(created?.data) : []; }; - /** - * Returns if ES|QL KB docs have been loaded - */ - public isESQLDocsLoaded = async (): Promise => { - const esqlDocs = await this.getKnowledgeBaseDocumentEntries({ - query: ESQL_DOCS_LOADED_QUERY, - // kbResource, // Note: `8.15` installs have kbResource as `unknown`, so don't filter yet - required: true, - }); - return esqlDocs.length > 0; - }; - /** * Returns if user's KB docs exists */ @@ -492,15 +392,12 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { } const esClient = await this.options.elasticsearchClientPromise; - const modelId = await this.options.getElserId(); try { const vectorSearchQuery = getKBVectorSearchQuery({ kbResource: USER_RESOURCE, required: false, user, - modelId, - v2KnowledgeBaseEnabled: this.options.v2KnowledgeBaseEnabled, }); const result = await esClient.search({ @@ -531,15 +428,12 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { const expectedDocsCount = await getSecurityLabsDocsCount({ logger: this.options.logger }); const esClient = await this.options.elasticsearchClientPromise; - const modelId = await this.options.getElserId(); try { const vectorSearchQuery = getKBVectorSearchQuery({ kbResource: SECURITY_LABS_RESOURCE, required: false, user, - modelId, - v2KnowledgeBaseEnabled: this.options.v2KnowledgeBaseEnabled, }); const result = await esClient.search({ @@ -585,7 +479,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { } const esClient = await this.options.elasticsearchClientPromise; - const modelId = await this.options.getElserId(); const vectorSearchQuery = getKBVectorSearchQuery({ filter, @@ -593,8 +486,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { query, required, user, - modelId, - v2KnowledgeBaseEnabled: this.options.v2KnowledgeBaseEnabled, }); try { @@ -605,14 +496,11 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { }); const results = result.hits.hits.map((hit) => { - const metadata = this.options.v2KnowledgeBaseEnabled - ? { - source: hit?._source?.source, - required: hit?._source?.required, - kbResource: hit?._source?.kb_resource, - } - : // @ts-ignore v1 schema has metadata nested in a `metadata` object and kbResource vs kb_resource - hit?._source?.metadata ?? {}; + const metadata = { + source: hit?._source?.source, + required: hit?._source?.required, + kbResource: hit?._source?.kb_resource, + }; return new Document({ pageContent: hit?._source?.text ?? '', metadata, @@ -691,7 +579,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { telemetry, global = false, }: { - knowledgeBaseEntry: KnowledgeBaseEntryCreateProps | LegacyKnowledgeBaseEntryCreateProps; + knowledgeBaseEntry: KnowledgeBaseEntryCreateProps; global?: boolean; telemetry: AnalyticsServiceSetup; }): Promise => { @@ -721,7 +609,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { knowledgeBaseEntry, global, telemetry, - isV2: this.options.v2KnowledgeBaseEnabled, }); }; @@ -732,10 +619,8 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { * is scoped to system user. */ public getAssistantTools = async ({ - assistantToolParams, esClient, }: { - assistantToolParams: AssistantToolParams; esClient: ElasticsearchClient; }): Promise => { const user = this.options.currentUser; @@ -746,9 +631,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { } try { - const elserId = this.isV2KnowledgeBaseEnabled - ? ASSISTANT_ELSER_INFERENCE_ID - : await this.options.getElserId(); + const elserId = ASSISTANT_ELSER_INFERENCE_ID; const userFilter = getKBUserFilter(user); const results = await this.findDocuments({ // Note: This is a magic number to set some upward bound as to not blow the context with too diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/ingest_pipeline.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/ingest_pipeline.ts index 8f459848af420..74da4d43d1400 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/ingest_pipeline.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/ingest_pipeline.ts @@ -5,31 +5,8 @@ * 2.0. */ -export const knowledgeBaseIngestPipeline = ({ - id, - modelId, - v2KnowledgeBaseEnabled, -}: { - id: string; - modelId: string; - v2KnowledgeBaseEnabled: boolean; -}) => ({ +export const knowledgeBaseIngestPipeline = ({ id }: { id: string }) => ({ id, description: 'Embedding pipeline for Elastic AI Assistant ELSER Knowledge Base', - processors: !v2KnowledgeBaseEnabled - ? [ - { - inference: { - if: 'ctx?.text != null', - model_id: modelId, - input_output: [ - { - input_field: 'text', - output_field: 'vector.tokens', - }, - ], - }, - }, - ] - : [], + processors: [], }); diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts index 93338174364fc..57b7745a89c78 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts @@ -53,8 +53,6 @@ export const pipelineExists = async ({ esClient, id }: PipelineExistsParams): Pr interface CreatePipelineParams { esClient: ElasticsearchClient; id: string; - modelId: string; - v2KnowledgeBaseEnabled: boolean; } /** @@ -63,22 +61,14 @@ interface CreatePipelineParams { * @param params params * @param params.esClient Elasticsearch client with privileges to check for ingest pipelines * @param params.id ID of the ingest pipeline - * @param params.modelId ID of the ELSER model * * @returns Promise indicating whether the pipeline was created */ -export const createPipeline = async ({ - esClient, - id, - modelId, - v2KnowledgeBaseEnabled, -}: CreatePipelineParams): Promise => { +export const createPipeline = async ({ esClient, id }: CreatePipelineParams): Promise => { try { const response = await esClient.ingest.putPipeline( knowledgeBaseIngestPipeline({ id, - modelId, - v2KnowledgeBaseEnabled, }) ); diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts index 15274f2323259..d7eff095b4be5 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts @@ -27,10 +27,7 @@ import { conversationsFieldMap } from '../ai_assistant_data_clients/conversation import { assistantPromptsFieldMap } from '../ai_assistant_data_clients/prompts/field_maps_configuration'; import { assistantAnonymizationFieldsFieldMap } from '../ai_assistant_data_clients/anonymization_fields/field_maps_configuration'; import { AIAssistantDataClient } from '../ai_assistant_data_clients'; -import { - knowledgeBaseFieldMap, - knowledgeBaseFieldMapV2, -} from '../ai_assistant_data_clients/knowledge_base/field_maps_configuration'; +import { knowledgeBaseFieldMap } from '../ai_assistant_data_clients/knowledge_base/field_maps_configuration'; import { AIAssistantKnowledgeBaseDataClient, GetAIAssistantKnowledgeBaseDataClientParams, @@ -85,8 +82,6 @@ export class AIAssistantService { private resourceInitializationHelper: ResourceInstallationHelper; private initPromise: Promise; private isKBSetupInProgress: boolean = false; - // Temporary 'feature flag' to determine if we should initialize the new kb mappings, toggled when accessing kbDataClient - private v2KnowledgeBaseEnabled: boolean = false; private hasInitializedV2KnowledgeBase: boolean = false; constructor(private readonly options: AIAssistantServiceOpts) { @@ -156,7 +151,7 @@ export class AIAssistantService { // Apply `default_pipeline` if pipeline exists for resource ...(resource in this.resourceNames.pipelines && // Remove this param and initialization when the `assistantKnowledgeBaseByDefault` feature flag is removed - !(resource === 'knowledgeBase' && this.v2KnowledgeBaseEnabled) + !(resource === 'knowledgeBase') ? { template: { settings: { @@ -185,16 +180,6 @@ export class AIAssistantService { pluginStop$: this.options.pluginStop$, }); - // If v2 is enabled, re-install data stream resources for new mappings - if (this.v2KnowledgeBaseEnabled) { - this.options.logger.debug(`Using V2 Knowledge Base Mappings`); - this.knowledgeBaseDataStream = this.createDataStream({ - resource: 'knowledgeBase', - kibanaVersion: this.options.kibanaVersion, - fieldMap: knowledgeBaseFieldMapV2, - }); - } - await this.knowledgeBaseDataStream.install({ esClient, logger: this.options.logger, @@ -206,28 +191,18 @@ export class AIAssistantService { esClient, id: this.resourceNames.pipelines.knowledgeBase, }); - // TODO: When FF is removed, ensure pipeline is re-created for those upgrading - if ( - // Install for v1 - (!this.v2KnowledgeBaseEnabled && !pipelineCreated) || - // Upgrade from v1 to v2 - (pipelineCreated && this.v2KnowledgeBaseEnabled) - ) { + // ensure pipeline is re-created for those upgrading + // pipeline is noop now, so if one does not exist we do not need one + if (pipelineCreated) { this.options.logger.debug( `Installing ingest pipeline - ${this.resourceNames.pipelines.knowledgeBase}` ); const response = await createPipeline({ esClient, id: this.resourceNames.pipelines.knowledgeBase, - modelId: await this.getElserId(), - v2KnowledgeBaseEnabled: this.v2KnowledgeBaseEnabled, }); this.options.logger.debug(`Installed ingest pipeline: ${response}`); - } else { - this.options.logger.debug( - `Ingest pipeline already exists - ${this.resourceNames.pipelines.knowledgeBase}` - ); } await this.promptsDataStream.install({ @@ -363,25 +338,16 @@ export class AIAssistantService { opts: CreateAIAssistantClientParams & GetAIAssistantKnowledgeBaseDataClientParams ): Promise { // If modelIdOverride is set, swap getElserId(), and ensure the pipeline is re-created with the correct model - if (opts.modelIdOverride != null) { + if (opts?.modelIdOverride != null) { const modelIdOverride = opts.modelIdOverride; this.getElserId = async () => modelIdOverride; } - // Note: Due to plugin lifecycle and feature flag registration timing, we need to pass in the feature flag here - // Remove this param and initialization when the `assistantKnowledgeBaseByDefault` feature flag is removed - if (opts.v2KnowledgeBaseEnabled) { - this.v2KnowledgeBaseEnabled = true; - } - - // If either v2 KB or a modelIdOverride is provided, we need to reinitialize all persistence resources to make sure + // If a V2 KnowledgeBase has never been initialized or a modelIdOverride is provided, we need to reinitialize all persistence resources to make sure // they're using the correct model/mappings. Technically all existing KB data is stale since it was created // with a different model/mappings, but modelIdOverride is only intended for testing purposes at this time // Added hasInitializedV2KnowledgeBase to prevent the console noise from re-init on each KB request - if ( - !this.hasInitializedV2KnowledgeBase && - (opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null) - ) { + if (!this.hasInitializedV2KnowledgeBase || opts?.modelIdOverride != null) { await this.initializeResources(); this.hasInitializedV2KnowledgeBase = true; } @@ -404,7 +370,6 @@ export class AIAssistantService { ml: this.options.ml, setIsKBSetupInProgress: this.setIsKBSetupInProgress.bind(this), spaceId: opts.spaceId, - v2KnowledgeBaseEnabled: opts.v2KnowledgeBaseEnabled ?? false, manageGlobalKnowledgeBaseAIAssistant: opts.manageGlobalKnowledgeBaseAIAssistant ?? false, }); } diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.test.ts deleted file mode 100644 index 4d32d7bc02da9..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.test.ts +++ /dev/null @@ -1,408 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; -import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; -import { - IndicesCreateResponse, - MlGetTrainedModelsStatsResponse, -} from '@elastic/elasticsearch/lib/api/types'; -import { Document } from 'langchain/document'; - -import { - ElasticsearchStore, - FALLBACK_SIMILARITY_SEARCH_SIZE, - TERMS_QUERY_SIZE, -} from './elasticsearch_store'; -import { mockMsearchResponse } from '../../../__mocks__/msearch_response'; -import { mockQueryText } from '../../../__mocks__/query_text'; -import { coreMock } from '@kbn/core/server/mocks'; -import { - KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT, - KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT, -} from '../../telemetry/event_based_telemetry'; -import { Metadata } from '@kbn/elastic-assistant-common'; - -jest.mock('uuid', () => ({ - v4: jest.fn(), -})); - -jest.mock('@kbn/core/server', () => ({ - ElasticsearchClient: jest.fn(), - Logger: jest.fn().mockImplementation(() => ({ - debug: jest.fn(), - error: jest.fn(), - info: jest.fn(), - })), -})); - -const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); -const mockLogger = loggingSystemMock.createLogger(); -const reportEvent = jest.fn(); -const mockTelemetry = { ...coreMock.createSetup().analytics, reportEvent }; -const KB_INDEX = '.elastic-assistant-kb'; - -const getElasticsearchStore = () => { - return new ElasticsearchStore(mockEsClient, KB_INDEX, mockLogger, mockTelemetry); -}; - -describe('ElasticsearchStore', () => { - let esStore: ElasticsearchStore; - - beforeEach(() => { - esStore = getElasticsearchStore(); - jest.clearAllMocks(); - }); - - describe('Index Management', () => { - it('Checks if index exists', async () => { - mockEsClient.indices.exists.mockResolvedValue(true); - - const exists = await esStore.indexExists(); - - expect(exists).toBe(true); - expect(mockEsClient.indices.exists).toHaveBeenCalledWith({ index: KB_INDEX }); - }); - - it('Creates an index', async () => { - mockEsClient.indices.create.mockResolvedValue({ - acknowledged: true, - } as IndicesCreateResponse); - - const created = await esStore.createIndex(); - - expect(created).toBe(true); - expect(mockEsClient.indices.create).toHaveBeenCalledWith({ - index: KB_INDEX, - mappings: { - properties: { - metadata: { - properties: { - kbResource: { type: 'keyword' }, - required: { type: 'boolean' }, - source: { type: 'keyword' }, - }, - }, - vector: { properties: { tokens: { type: 'rank_features' } } }, - }, - }, - settings: { default_pipeline: '.kibana-elastic-ai-assistant-kb-ingest-pipeline' }, - }); - }); - - it('Deletes an index', async () => { - mockEsClient.indices.delete.mockResolvedValue({ acknowledged: true }); - - const deleted = await esStore.deleteIndex(); - - expect(deleted).toBe(true); - expect(mockEsClient.indices.delete).toHaveBeenCalledWith({ index: KB_INDEX }); - }); - }); - - describe('Pipeline Management', () => { - it('Checks if pipeline exists', async () => { - mockEsClient.ingest.getPipeline.mockResolvedValue({}); - - const exists = await esStore.pipelineExists(); - - expect(exists).toBe(false); - expect(mockEsClient.ingest.getPipeline).toHaveBeenCalledWith({ - id: '.kibana-elastic-ai-assistant-kb-ingest-pipeline', - }); - }); - - it('Creates an ingest pipeline', async () => { - mockEsClient.ingest.putPipeline.mockResolvedValue({ acknowledged: true }); - - const created = await esStore.createPipeline(); - - expect(created).toBe(true); - expect(mockEsClient.ingest.putPipeline).toHaveBeenCalledWith({ - description: 'Embedding pipeline for Elastic AI Assistant ELSER Knowledge Base', - id: '.kibana-elastic-ai-assistant-kb-ingest-pipeline', - processors: [ - { - inference: { - field_map: { text: 'text_field' }, - inference_config: { text_expansion: { results_field: 'tokens' } }, - model_id: '.elser_model_2', - target_field: 'vector', - }, - }, - ], - }); - }); - - it('Deletes an ingest pipeline', async () => { - mockEsClient.ingest.deletePipeline.mockResolvedValue({ acknowledged: true }); - - const deleted = await esStore.deletePipeline(); - - expect(deleted).toBe(true); - expect(mockEsClient.ingest.deletePipeline).toHaveBeenCalledWith({ - id: '.kibana-elastic-ai-assistant-kb-ingest-pipeline', - }); - }); - }); - - describe('isModelInstalled', () => { - it('returns true if model is started and fully allocated', async () => { - mockEsClient.ml.getTrainedModelsStats.mockResolvedValue({ - trained_model_stats: [ - { - deployment_stats: { - state: 'started', - allocation_status: { - state: 'fully_allocated', - }, - }, - }, - ], - } as MlGetTrainedModelsStatsResponse); - - const isInstalled = await esStore.isModelInstalled('.elser_model_2'); - - expect(isInstalled).toBe(true); - expect(mockEsClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ - model_id: '.elser_model_2', - }); - }); - - it('returns false if model is not started', async () => { - mockEsClient.ml.getTrainedModelsStats.mockResolvedValue({ - trained_model_stats: [ - { - deployment_stats: { - state: 'starting', - allocation_status: { - state: 'fully_allocated', - }, - }, - }, - ], - } as MlGetTrainedModelsStatsResponse); - - const isInstalled = await esStore.isModelInstalled('.elser_model_2'); - - expect(isInstalled).toBe(false); - expect(mockEsClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ - model_id: '.elser_model_2', - }); - }); - - it('returns false if model is not fully allocated', async () => { - mockEsClient.ml.getTrainedModelsStats.mockResolvedValue({ - trained_model_stats: [ - { - deployment_stats: { - state: 'started', - allocation_status: { - state: 'starting', - }, - }, - }, - ], - } as MlGetTrainedModelsStatsResponse); - - const isInstalled = await esStore.isModelInstalled('.elser_model_2'); - - expect(isInstalled).toBe(false); - expect(mockEsClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ - model_id: '.elser_model_2', - }); - }); - }); - - describe('addDocuments', () => { - it('Checks if documents are added', async () => { - mockEsClient.bulk.mockResolvedValue({ - errors: false, - took: 515, - ingest_took: 4026, - items: [ - { - index: { - _index: '.kibana-elastic-ai-assistant-kb', - _id: 'be2584a9-ad2e-4f13-a11c-c0b79423079c', - _version: 2, - result: 'updated', - forced_refresh: true, - _shards: { - total: 2, - successful: 1, - failed: 0, - }, - _seq_no: 1, - _primary_term: 1, - status: 200, - }, - }, - ], - }); - - const document = new Document({ - pageContent: 'interesting stuff', - metadata: { kbResource: 'esql', required: false, source: '1' }, - }); - - const docsInstalled = await esStore.addDocuments([document]); - - expect(docsInstalled).toStrictEqual(['be2584a9-ad2e-4f13-a11c-c0b79423079c']); - expect(mockEsClient.bulk).toHaveBeenCalledWith({ - operations: [ - { - index: { - _id: undefined, - _index: '.elastic-assistant-kb', - }, - }, - { - metadata: { - kbResource: 'esql', - required: false, - source: '1', - }, - text: 'interesting stuff', - }, - ], - refresh: true, - }); - }); - }); - - describe('similaritySearch', () => { - it('Checks if documents are found', async () => { - mockEsClient.msearch.mockResolvedValue(mockMsearchResponse); - - const searchResults = await esStore.similaritySearch(mockQueryText); - - expect(searchResults).toStrictEqual([ - { - pageContent: - "[[esql-from]]\n=== `FROM`\n\nThe `FROM` source command returns a table with up to 10,000 documents from a\ndata stream, index, or alias. Each row in the resulting table represents a\ndocument. Each column corresponds to a field, and can be accessed by the name\nof that field.\n\n[source,esql]\n----\nFROM employees\n----\n\nYou can use <> to refer to indices, aliases\nand data streams. This can be useful for time series data, for example to access\ntoday's index:\n\n[source,esql]\n----\nFROM \n----\n\nUse comma-separated lists or wildcards to query multiple data streams, indices,\nor aliases:\n\n[source,esql]\n----\nFROM employees-00001,employees-*\n----\n", - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc', - }, - }, - { - pageContent: - '[[esql-example-queries]]\n\nThe following is an example an ES|QL query:\n\n```\nFROM logs-*\n| WHERE NOT CIDR_MATCH(destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")\n| STATS destcount = COUNT(destination.ip) by user.name, host.name\n| ENRICH ldap_lookup_new ON user.name\n| WHERE group.name IS NOT NULL\n| EVAL follow_up = CASE(\n destcount >= 100, "true",\n "false")\n| SORT destcount desc\n| KEEP destcount, host.name, user.name, group.name, follow_up\n```\n', - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/example_queries/esql_example_query_0001.asciidoc', - }, - }, - ]); - - expect(mockEsClient.msearch).toHaveBeenCalledWith({ - body: [ - { - index: '.elastic-assistant-kb', - }, - { - query: { - bool: { - must_not: [{ term: { 'metadata.required': true } }], - must: [ - { - text_expansion: { - 'vector.tokens': { - model_id: '.elser_model_2', - model_text: mockQueryText, - }, - }, - }, - ], - }, - }, - size: FALLBACK_SIMILARITY_SEARCH_SIZE, // <-- `FALLBACK_SIMILARITY_SEARCH_SIZE` is used when `k` is not provided - }, - { - index: '.elastic-assistant-kb', - }, - { - query: { - bool: { - must: [{ term: { 'metadata.required': true } }], - }, - }, - size: TERMS_QUERY_SIZE, - }, - ], - }); - }); - - it('uses the value of `k` instead of the `FALLBACK_SIMILARITY_SEARCH_SIZE` when `k` is provided', async () => { - mockEsClient.msearch.mockResolvedValue(mockMsearchResponse); - - const k = 4; - await esStore.similaritySearch(mockQueryText, k); - - expect(mockEsClient.msearch).toHaveBeenCalledWith({ - body: [ - { - index: '.elastic-assistant-kb', - }, - { - query: { - bool: { - must_not: [{ term: { 'metadata.required': true } }], - must: [ - { - text_expansion: { - 'vector.tokens': { - model_id: '.elser_model_2', - model_text: mockQueryText, - }, - }, - }, - ], - }, - }, - size: k, // <-- `k` is used instead of `FALLBACK_SIMILARITY_SEARCH_SIZE` - }, - { - index: '.elastic-assistant-kb', - }, - { - query: { - bool: { - must: [{ term: { 'metadata.required': true } }], - }, - }, - size: TERMS_QUERY_SIZE, - }, - ], - }); - }); - - it('Reports successful telemetry event', async () => { - mockEsClient.msearch.mockResolvedValue(mockMsearchResponse); - - await esStore.similaritySearch(mockQueryText); - - expect(reportEvent).toHaveBeenCalledWith(KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT.eventType, { - model: '.elser_model_2', - responseTime: 142, - resultCount: 2, - }); - }); - - it('Reports error telemetry event', async () => { - mockEsClient.msearch.mockRejectedValue(new Error('Oh no!')); - - await esStore.similaritySearch(mockQueryText); - - expect(reportEvent).toHaveBeenCalledWith(KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT.eventType, { - model: '.elser_model_2', - errorMessage: 'Oh no!', - }); - }); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.ts deleted file mode 100644 index 78c1b104685ad..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/elasticsearch_store.ts +++ /dev/null @@ -1,478 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { type AnalyticsServiceSetup, ElasticsearchClient, Logger } from '@kbn/core/server'; -import { - MappingTypeMapping, - MlTrainedModelDeploymentNodesStats, - MlTrainedModelStats, -} from '@elastic/elasticsearch/lib/api/types'; -import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Callbacks } from '@langchain/core/callbacks/manager'; -import { Document } from 'langchain/document'; -import { VectorStore } from '@langchain/core/vectorstores'; -import * as uuid from 'uuid'; - -import { Metadata } from '@kbn/elastic-assistant-common'; -import { transformError } from '@kbn/securitysolution-es-utils'; -import { ElasticsearchEmbeddings } from '../embeddings/elasticsearch_embeddings'; -import { FlattenedHit, getFlattenedHits } from './helpers/get_flattened_hits'; -import { getMsearchQueryBody } from './helpers/get_msearch_query_body'; -import { getTermsSearchQuery } from './helpers/get_terms_search_query'; -import { getVectorSearchQuery } from './helpers/get_vector_search_query'; -import type { MsearchResponse } from './helpers/types'; -import { - KNOWLEDGE_BASE_INDEX_PATTERN, - KNOWLEDGE_BASE_INGEST_PIPELINE, -} from '../../../routes/knowledge_base/constants'; -import { getRequiredKbDocsTermsQueryDsl } from './helpers/get_required_kb_docs_terms_query_dsl'; -import { - KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT, - KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT, -} from '../../telemetry/event_based_telemetry'; -import { AIAssistantKnowledgeBaseDataClient } from '../../../ai_assistant_data_clients/knowledge_base'; - -interface CreatePipelineParams { - id?: string; - description?: string; -} - -interface CreateIndexParams { - index?: string; - pipeline?: string; -} - -/** - * A fallback for the query `size` that determines how many documents to - * return from Elasticsearch when performing a similarity search. - * - * The size is typically determined by the implementation of LangChain's - * `VectorStoreRetriever._getRelevantDocuments` function, so this fallback is - * only required when using the `ElasticsearchStore` directly. - */ -export const FALLBACK_SIMILARITY_SEARCH_SIZE = 10; - -/** The maximum number of hits to return from a `terms` query, via the `size` parameter */ -export const TERMS_QUERY_SIZE = 10000; - -/** - * Basic ElasticsearchStore implementation only leveraging ELSER for storage and retrieval. - */ -export class ElasticsearchStore extends VectorStore { - declare FilterType: QueryDslQueryContainer; - - private readonly esClient: ElasticsearchClient; - private readonly kbDataClient: AIAssistantKnowledgeBaseDataClient | undefined; - private readonly index: string; - private readonly logger: Logger; - private readonly telemetry: AnalyticsServiceSetup; - private readonly model: string; - private kbResource?: string; - - _vectorstoreType(): string { - return 'elasticsearch'; - } - - constructor( - esClient: ElasticsearchClient, - index: string, - logger: Logger, - telemetry: AnalyticsServiceSetup, - model?: string, - kbResource?: string | undefined, - kbDataClient?: AIAssistantKnowledgeBaseDataClient - ) { - super(new ElasticsearchEmbeddings(logger), { esClient, index }); - this.esClient = esClient; - this.index = index ?? KNOWLEDGE_BASE_INDEX_PATTERN; - this.logger = logger; - this.telemetry = telemetry; - this.model = model ?? '.elser_model_2'; - this.kbResource = kbResource; - this.kbDataClient = kbDataClient; - } - - setKbResource(kbResource: string) { - this.kbResource = kbResource; - } - - /** - * Add documents to the store. Embeddings are created on ingest into index configured with - * ELSER ingest pipeline. Returns a list of document IDs. - * - * @param documents Documents to add to the store - * @param options Any additional options as defined in the interface - * @returns Promise of document IDs added to the store - */ - addDocuments = async ( - documents: Array>, - options?: Record - ): Promise => { - // Code path for when `assistantKnowledgeBaseByDefault` FF is enabled - // Once removed replace addDocuments() w/ addDocumentsViaDataClient() - if (this.kbDataClient != null) { - return this.addDocumentsViaDataClient(documents, options); - } - - const pipelineExists = await this.pipelineExists(); - if (!pipelineExists) { - await this.createPipeline(); - } - - const operations = documents.flatMap(({ pageContent, metadata }) => [ - { index: { _index: this.index, _id: uuid.v4() } }, - { text: pageContent, metadata }, - ]); - - try { - const response = await this.esClient.bulk({ refresh: true, operations }); - this.logger.debug(() => `Add Documents Response:\n ${JSON.stringify(response)}`); - - const errorIds = response.items.filter((i) => i.index?.error != null); - operations.forEach((op, i) => { - if (errorIds.some((e) => e.index?._id === op.index?._id)) { - this.logger.error(`Error adding document to KB: ${JSON.stringify(operations?.[i + 1])}`); - } - }); - - return response.items.flatMap((i) => - i.index?._id != null && i.index.error == null ? [i.index._id] : [] - ); - } catch (e) { - this.logger.error(`Error loading data into KB\n ${e}`); - return []; - } - }; - - addDocumentsViaDataClient = async ( - documents: Array>, - options?: Record - ): Promise => { - if (!this.kbDataClient) { - this.logger.error('No kbDataClient provided'); - return []; - } - - try { - const response = await this.kbDataClient.addKnowledgeBaseDocuments({ - documents, - global: true, - }); - return response.map((doc) => doc.id); - } catch (e) { - this.logger.error(`Error loading data into KB\n ${e}`); - return []; - } - }; - - /** - * Add vectors to the store. Returns a list of document IDs. - * - * @param vectors Vector representation of documents to add to the store - * @param documents Documents corresponding to the provided vectors - * @param options Any additional options as defined in the interface - * @returns Promise of document IDs added to the store - */ - addVectors = ( - vectors: number[][], - documents: Document[], - options?: {} - ): Promise => { - // Note: implement if/when needed - this.logger.info('ElasticsearchStore.addVectors not implemented'); - return Promise.resolve(undefined); - }; - - /** - * Performs similarity search on the store using the provided query vector and filter, returning k similar - * documents along with their score. - * - * @param query Query vector to search with - * @param k Number of similar documents to return - * @param filter Optional filter to apply to the search - * - * @returns Promise> of similar documents and their scores - */ - similaritySearchVectorWithScore = ( - query: number[], - k: number, - filter?: this['FilterType'] - ): Promise> => { - // Note: Implement if needed - this.logger.info('ElasticsearchStore.similaritySearchVectorWithScore not implemented'); - return Promise.resolve([]); - }; - - // Non-abstract function overrides - - /** - * Performs similarity search on the store using the provided query string and filter, returning k similar - * @param query Query vector to search with - * @param k Number of similar documents to return - * @param filter Optional filter to apply to the search - * @param _callbacks Optional callbacks - * @param filterRequiredDocs Optional whether or not to exclude the required docs filter - * - * Fun facts: - * - This function is called by LangChain's `VectorStoreRetriever._getRelevantDocuments` - * - The `k` parameter is typically determined by LangChain's `VectorStoreRetriever._getRelevantDocuments`, and has been observed to default to `4` in the wild (see langchain/dist/vectorstores/base.ts) - * @returns Promise of similar documents - */ - similaritySearch = async ( - query: string, - k?: number, - filter?: this['FilterType'] | undefined, - _callbacks?: Callbacks | undefined, - filterRequiredDocs = true - ): Promise => { - // requiredDocs is an array of filters that can be used in a `bool` Elasticsearch DSL query to filter in/out required KB documents: - const requiredDocs = filterRequiredDocs ? getRequiredKbDocsTermsQueryDsl(this.kbResource) : []; - - // The `k` parameter is typically provided by LangChain's `VectorStoreRetriever._getRelevantDocuments`, which calls this function: - const vectorSearchQuerySize = k ?? FALLBACK_SIMILARITY_SEARCH_SIZE; - - // build a vector search query: - const vectorSearchQuery = getVectorSearchQuery({ - filter, - modelId: this.model, - mustNotTerms: requiredDocs, - query, - }); - - // build a (separate) terms search query: - const termsSearchQuery = getTermsSearchQuery(requiredDocs); - - // combine the vector search query and the terms search queries into a single multi-search query: - const mSearchQueryBody = getMsearchQueryBody({ - index: this.index, - termsSearchQuery, - termsSearchQuerySize: TERMS_QUERY_SIZE, - vectorSearchQuery, - vectorSearchQuerySize, - }); - - try { - // execute both queries via a single multi-search request: - const result = await this.esClient.msearch(mSearchQueryBody); - - // flatten the results of the combined queries into a single array of hits: - const results: FlattenedHit[] = result.responses.flatMap((response) => { - const maybeEsqlMsearchResponse: MsearchResponse = response as MsearchResponse; - - return getFlattenedHits(maybeEsqlMsearchResponse); - }); - - this.telemetry.reportEvent(KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT.eventType, { - model: this.model, - ...(this.kbResource != null ? { resourceAccessed: this.kbResource } : {}), - resultCount: results.length, - responseTime: result.took ?? 0, - }); - - this.logger.debug( - () => - `Similarity search metadata source:\n${JSON.stringify( - results.map((r) => r?.metadata?.source ?? '(missing metadata.source)'), - null, - 2 - )}` - ); - - return results; - } catch (e) { - const error = transformError(e); - this.telemetry.reportEvent(KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT.eventType, { - model: this.model, - ...(this.kbResource != null ? { resourceAccessed: this.kbResource } : {}), - errorMessage: error.message, - }); - this.logger.error(e); - return []; - } - }; - - // ElasticsearchStore explicit utility functions - - /** - * Checks if the provided index exists in Elasticsearch - * - * @returns Promise indicating whether the index exists - * @param index Index to check - * @returns Promise indicating whether the index exists - */ - indexExists = async (index?: string): Promise => { - return this.esClient.indices.exists({ index: index ?? this.index }); - }; - - /** - * Create index for ELSER embeddings in Elasticsearch - * - * @returns Promise indicating whether the index was created - */ - createIndex = async ({ index, pipeline }: CreateIndexParams = {}): Promise => { - const mappings: MappingTypeMapping = { - properties: { - metadata: { - properties: { - /** the category of knowledge, e.g. `esql` */ - kbResource: { type: 'keyword' }, - /** when `true`, return this document in all searches for the `kbResource` */ - required: { type: 'boolean' }, - /** often a file path when the document was created via a LangChain `DirectoryLoader`, this metadata describes the origin of the document */ - source: { type: 'keyword' }, - }, - }, - vector: { - properties: { tokens: { type: 'rank_features' } }, - }, - }, - }; - - const settings = { default_pipeline: pipeline ?? KNOWLEDGE_BASE_INGEST_PIPELINE }; - - const response = await this.esClient.indices.create({ - index: index ?? this.index, - mappings, - settings, - }); - - return response.acknowledged; - }; - - /** - * Delete index for ELSER embeddings in Elasticsearch - * @param index Index to delete, otherwise uses the default index - * - * @returns Promise indicating whether the index was created - */ - deleteIndex = async (index?: string): Promise => { - // Code path for when `assistantKnowledgeBaseByDefault` FF is enabled - // We won't be supporting delete operations for the KB data stream going forward, so this can be removed along with the FF - if (this.kbDataClient != null) { - const response = await this.esClient.indices.deleteDataStream({ name: index ?? this.index }); - return response.acknowledged; - } - - const response = await this.esClient.indices.delete({ - index: index ?? this.index, - }); - - return response.acknowledged; - }; - - /** - * Checks if the provided ingest pipeline exists in Elasticsearch - * - * @param pipelineId ID of the ingest pipeline to check - * @returns Promise indicating whether the pipeline exists - */ - pipelineExists = async (pipelineId?: string): Promise => { - try { - const id = - pipelineId ?? - this.kbDataClient?.options.ingestPipelineResourceName ?? - KNOWLEDGE_BASE_INGEST_PIPELINE; - const response = await this.esClient.ingest.getPipeline({ - id, - }); - return Object.keys(response).length > 0; - } catch (e) { - // The GET /_ingest/pipeline/{pipelineId} API returns an empty object w/ 404 Not Found. - return false; - } - }; - - /** - * Create ingest pipeline for ELSER in Elasticsearch - * - * @returns Promise indicating whether the pipeline was created - */ - createPipeline = async ({ id, description }: CreatePipelineParams = {}): Promise => { - const response = await this.esClient.ingest.putPipeline({ - id: - id ?? - this.kbDataClient?.options.ingestPipelineResourceName ?? - KNOWLEDGE_BASE_INGEST_PIPELINE, - description: - description ?? 'Embedding pipeline for Elastic AI Assistant ELSER Knowledge Base', - processors: [ - { - inference: { - model_id: this.model, - target_field: 'vector', - field_map: { - text: 'text_field', - }, - inference_config: { - // @ts-expect-error - text_expansion: { - results_field: 'tokens', - }, - }, - }, - }, - ], - }); - - return response.acknowledged; - }; - - /** - * Delete ingest pipeline for ELSER in Elasticsearch - * - * @returns Promise indicating whether the pipeline was created - */ - deletePipeline = async (pipelineId?: string): Promise => { - const response = await this.esClient.ingest.deletePipeline({ - id: - pipelineId ?? - this.kbDataClient?.options.ingestPipelineResourceName ?? - KNOWLEDGE_BASE_INGEST_PIPELINE, - }); - - return response.acknowledged; - }; - - /** - * Checks if the provided model is installed in Elasticsearch - * - * @param modelId ID of the model to check - * @returns Promise indicating whether the model is installed - */ - async isModelInstalled(modelId?: string): Promise { - try { - // Code path for when `assistantKnowledgeBaseByDefault` FF is enabled - if (this.kbDataClient != null) { - // esStore.isModelInstalled() is actually checking if the model is deployed, not installed, so do that instead - return this.kbDataClient.isModelDeployed(); - } - - const getResponse = await this.esClient.ml.getTrainedModelsStats({ - model_id: modelId ?? this.model, - }); - - this.logger.debug(`modelId: ${modelId}`); - - // For standardized way of checking deployment status see: https://github.com/elastic/elasticsearch/issues/106986 - const isReadyESS = (stats: MlTrainedModelStats) => - stats.deployment_stats?.state === 'started' && - stats.deployment_stats?.allocation_status.state === 'fully_allocated'; - - const isReadyServerless = (stats: MlTrainedModelStats) => - (stats.deployment_stats?.nodes as unknown as MlTrainedModelDeploymentNodesStats[]).some( - (node) => node.routing_state.routing_state === 'started' - ); - - return getResponse.trained_model_stats.some( - (stats) => isReadyESS(stats) || isReadyServerless(stats) - ); - } catch (e) { - // Returns 404 if it doesn't exist - return false; - } - } -} diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_flattened_hits.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_flattened_hits.test.ts deleted file mode 100644 index cc08a95cdb532..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_flattened_hits.test.ts +++ /dev/null @@ -1,81 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getFlattenedHits } from './get_flattened_hits'; -import { mockMsearchResponse } from '../../../../__mocks__/msearch_response'; -import type { MsearchResponse } from './types'; - -describe('getFlattenedHits', () => { - it('returns an empty array when the response is undefined', () => { - const result = getFlattenedHits(undefined); - - expect(result).toEqual([]); - }); - - it('returns an empty array when hits > hits is empty', () => { - const result = getFlattenedHits({ hits: { hits: [] } }); - - expect(result).toEqual([]); - }); - - it('returns the expected flattened hits given a non-empty `MsearchResponse`', () => { - const expected = [ - { - pageContent: - "[[esql-from]]\n=== `FROM`\n\nThe `FROM` source command returns a table with up to 10,000 documents from a\ndata stream, index, or alias. Each row in the resulting table represents a\ndocument. Each column corresponds to a field, and can be accessed by the name\nof that field.\n\n[source,esql]\n----\nFROM employees\n----\n\nYou can use <> to refer to indices, aliases\nand data streams. This can be useful for time series data, for example to access\ntoday's index:\n\n[source,esql]\n----\nFROM \n----\n\nUse comma-separated lists or wildcards to query multiple data streams, indices,\nor aliases:\n\n[source,esql]\n----\nFROM employees-00001,employees-*\n----\n", - metadata: { - source: - '/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc', - }, - }, - ]; - - const result = getFlattenedHits(mockMsearchResponse.responses[0] as MsearchResponse); - - expect(result).toEqual(expected); - }); - - it('returns an array of FlattenedHits with empty strings when given an MsearchResponse with missing fields', () => { - const msearchResponse = { - hits: { - hits: [ - { - _source: { - metadata: { - source: '/source/1', - }, - }, - }, - { - _source: { - text: 'Source 2 text', - }, - }, - ], - }, - }; - - const expected = [ - { - pageContent: '', // <-- missing text field - metadata: { - source: '/source/1', - }, - }, - { - pageContent: 'Source 2 text', - metadata: { - source: '', // <-- missing source field - }, - }, - ]; - - const result = getFlattenedHits(msearchResponse); - - expect(result).toEqual(expected); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_flattened_hits.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_flattened_hits.ts deleted file mode 100644 index f6c3a3ef0e9fa..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_flattened_hits.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { MsearchKbHit, MsearchResponse } from './types'; - -/** - * Represents a flattened hit from an Elasticsearch Msearch response - * - * It contains the page content and metadata source of a KB document - */ -export interface FlattenedHit { - pageContent: string; - metadata: { - source: string; - }; -} - -/** - * Returns an array of flattened hits from the specified Msearch response - * that contain the page content and metadata source of KB documents - * - * @param maybeMsearchResponse An Elasticsearch Msearch response, which returns the results of multiple searches in a single request - * @returns Returns an array of flattened hits from the specified Msearch response that contain the page content and metadata source of KB documents - */ -export const getFlattenedHits = ( - maybeMsearchResponse: MsearchResponse | undefined -): FlattenedHit[] => - maybeMsearchResponse?.hits?.hits?.flatMap((hit: MsearchKbHit) => ({ - pageContent: hit?._source?.text ?? '', - metadata: { - source: hit?._source?.metadata?.source ?? '', - }, - })) ?? []; diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_msearch_query_body.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_msearch_query_body.test.ts deleted file mode 100644 index 2697aaf76a085..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_msearch_query_body.test.ts +++ /dev/null @@ -1,46 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { TERMS_QUERY_SIZE } from '../elasticsearch_store'; -import { getMsearchQueryBody } from './get_msearch_query_body'; -import { mockTermsSearchQuery } from '../../../../__mocks__/terms_search_query'; -import { mockVectorSearchQuery } from '../../../../__mocks__/vector_search_query'; - -describe('getMsearchQueryBody', () => { - it('returns the expected multi-search request body', () => { - const index = '.kibana-elastic-ai-assistant-kb'; - - const vectorSearchQuery = mockVectorSearchQuery; - const vectorSearchQuerySize = 4; - - const termsSearchQuery = mockTermsSearchQuery; - const termsSearchQuerySize = TERMS_QUERY_SIZE; - - const result = getMsearchQueryBody({ - index, - termsSearchQuery, - termsSearchQuerySize, - vectorSearchQuery, - vectorSearchQuerySize, - }); - - expect(result).toEqual({ - body: [ - { index }, - { - query: mockVectorSearchQuery, - size: vectorSearchQuerySize, - }, - { index }, - { - query: mockTermsSearchQuery, - size: TERMS_QUERY_SIZE, - }, - ], - }); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_msearch_query_body.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_msearch_query_body.ts deleted file mode 100644 index c93c3f2e30954..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_msearch_query_body.ts +++ /dev/null @@ -1,67 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; - -/** - * Represents an entry in a multi-search request body that specifies the name of an index to search - */ -export interface MsearchQueryBodyIndexEntry { - index: string; -} - -/** - * Represents an entry in a multi-search request body that specifies a query to execute - */ -export interface MsearchQueryBodyQueryEntry { - query: QueryDslQueryContainer; - size: number; -} - -/** - * Represents a multi-search request body, which returns the results of multiple searches in a single request - */ -export interface MsearchQueryBody { - body: Array; -} - -/** - * Returns a multi-search request body, which returns the results of multiple searches in a single request - * - * @param index The KB index to search, e.g. `.kibana-elastic-ai-assistant-kb` - * @param termsSearchQuery An Elasticsearch DSL query that performs a terms search, typically used to search for required KB documents - * @param termsSearchQuerySize The maximum number of required KB documents to return - * @param vectorSearchQuery An Elasticsearch DSL query that performs a vector search, typically used to search for similar KB documents - * @param vectorSearchQuerySize The maximum number of similar KB documents to return - * @returns A multi-search request body, which returns the results of multiple searches in a single request - */ -export const getMsearchQueryBody = ({ - index, - termsSearchQuery, - termsSearchQuerySize, - vectorSearchQuery, - vectorSearchQuerySize, -}: { - index: string; - termsSearchQuery: QueryDslQueryContainer; - termsSearchQuerySize: number; - vectorSearchQuery: QueryDslQueryContainer; - vectorSearchQuerySize: number; -}): MsearchQueryBody => ({ - body: [ - { index }, - { - query: vectorSearchQuery, - size: vectorSearchQuerySize, - }, - { index }, - { - query: termsSearchQuery, - size: termsSearchQuerySize, - }, - ], -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.test.ts deleted file mode 100644 index 5c4f944e83178..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.test.ts +++ /dev/null @@ -1,21 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getRequiredKbDocsTermsQueryDsl } from './get_required_kb_docs_terms_query_dsl'; - -const kbResource = 'esql'; - -describe('getRequiredKbDocsTermsQueryDsl', () => { - it('returns the expected terms query DSL', () => { - const result = getRequiredKbDocsTermsQueryDsl(kbResource); - - expect(result).toEqual([ - { term: { 'metadata.kbResource': 'esql' } }, - { term: { 'metadata.required': true } }, - ]); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.ts deleted file mode 100644 index df3e8f42ad63b..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_required_kb_docs_terms_query_dsl.ts +++ /dev/null @@ -1,39 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Field, FieldValue, QueryDslTermQuery } from '@elastic/elasticsearch/lib/api/types'; - -/** - * For the specified topic, returns an array of filters that can be used in a - * `bool` Elasticsearch DSL query to filter in/out required KB documents. - * - * The returned filters can be used in different types of queries to, for example: - * - To filter out required KB documents from a vector search - * - To filter in required KB documents in a terms query - * - * @param kbResource Search for required KB documents for this topic - * - * @returns An array of `term`s that may be used in a `bool` Elasticsearch DSL query to filter in/out required KB documents - */ -export const getRequiredKbDocsTermsQueryDsl = ( - kbResource?: string -): Array>> => [ - ...(kbResource != null - ? [ - { - term: { - 'metadata.kbResource': kbResource, - }, - }, - ] - : []), - { - term: { - 'metadata.required': true, - }, - }, -]; diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_terms_search_query.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_terms_search_query.test.ts deleted file mode 100644 index 98d3b2c5d36c2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_terms_search_query.test.ts +++ /dev/null @@ -1,21 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getTermsSearchQuery } from './get_terms_search_query'; -import { mockTerms } from '../../../../__mocks__/terms'; - -describe('getTermsSearchQuery', () => { - it('returns the expected Elasticsearch query DSL', () => { - const query = getTermsSearchQuery(mockTerms); - - expect(query).toEqual({ - bool: { - must: mockTerms, - }, - }); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_terms_search_query.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_terms_search_query.ts deleted file mode 100644 index 8fcc7b3b20851..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_terms_search_query.ts +++ /dev/null @@ -1,29 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - Field, - FieldValue, - QueryDslTermQuery, - QueryDslQueryContainer, -} from '@elastic/elasticsearch/lib/api/types'; - -/** - * Returns an Elasticsearch DSL query that performs a terms search, - * such that all of the specified terms must be present in the search results. - * - * @param mustTerms All of the specified terms must be present in the search results - * - * @returns An Elasticsearch DSL query that performs a terms search, such that all of the specified terms must be present in the search results - */ -export const getTermsSearchQuery = ( - mustTerms: Array>> -): QueryDslQueryContainer => ({ - bool: { - must: [...mustTerms], // all of the specified terms must be present in the search results - }, -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_vector_search_query.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_vector_search_query.test.ts deleted file mode 100644 index da6a7227953f2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_vector_search_query.test.ts +++ /dev/null @@ -1,129 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; - -import { getVectorSearchQuery } from './get_vector_search_query'; -import { mockTerms } from '../../../../__mocks__/terms'; -import { mockQueryText } from '../../../../__mocks__/query_text'; - -describe('getVectorSearchQuery', () => { - it('returns the expected query when mustNotTerms is empty', () => { - const result = getVectorSearchQuery({ - filter: undefined, - modelId: '.elser_model_2', - mustNotTerms: [], // <--- empty - query: mockQueryText, - }); - - expect(result).toEqual({ - bool: { - filter: undefined, - must: [ - { - text_expansion: { - 'vector.tokens': { - model_id: '.elser_model_2', - model_text: - 'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called follow_up that contains a value of true, otherwise, it should contain false. The user names should also be enriched with their respective group names.', - }, - }, - }, - ], - must_not: [], - }, - }); - }); - - it('returns the expected query when mustNotTerms are provided', () => { - const result = getVectorSearchQuery({ - filter: undefined, - modelId: '.elser_model_2', - mustNotTerms: mockTerms, // <--- mock terms - query: mockQueryText, - }); - - expect(result).toEqual({ - bool: { - filter: undefined, - must: [ - { - text_expansion: { - 'vector.tokens': { - model_id: '.elser_model_2', - model_text: - 'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called follow_up that contains a value of true, otherwise, it should contain false. The user names should also be enriched with their respective group names.', - }, - }, - }, - ], - must_not: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], - }, - }); - }); - - it('returns the expected results when a filter is provided', () => { - const filter: QueryDslQueryContainer = { - bool: { - must: [ - { - term: { - 'some.field': 'value', - }, - }, - ], - }, - }; - - const result = getVectorSearchQuery({ - filter, - modelId: '.elser_model_2', - mustNotTerms: mockTerms, // <--- mock terms - query: mockQueryText, - }); - - expect(result).toEqual({ - bool: { - filter, - must: [ - { - text_expansion: { - 'vector.tokens': { - model_id: '.elser_model_2', - model_text: - 'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called follow_up that contains a value of true, otherwise, it should contain false. The user names should also be enriched with their respective group names.', - }, - }, - }, - ], - must_not: [ - { - term: { - 'metadata.kbResource': 'esql', - }, - }, - { - term: { - 'metadata.required': true, - }, - }, - ], - }, - }); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_vector_search_query.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_vector_search_query.ts deleted file mode 100644 index 613ee5c501560..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/get_vector_search_query.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - Field, - FieldValue, - QueryDslQueryContainer, - QueryDslTermQuery, -} from '@elastic/elasticsearch/lib/api/types'; - -/** - * Returns an Elasticsearch query DSL that performs a vector search - * that excludes a set of documents from the search results. - * - * @param filter Optional filter to apply to the search - * @param modelId ID of the model to search with, e.g. `.elser_model_2` - * @param mustNotTerms Array of objects that may be used in a `bool` Elasticsearch DSL query to, for example, exclude the required KB docs from the vector search, so there's no overlap - * @param query The search query provided by the user - * @returns - */ -export const getVectorSearchQuery = ({ - filter, - modelId, - mustNotTerms, - query, -}: { - filter: QueryDslQueryContainer | undefined; - modelId: string; - mustNotTerms: Array>>; - query: string; -}): QueryDslQueryContainer => ({ - bool: { - must_not: [...mustNotTerms], - must: [ - { - text_expansion: { - 'vector.tokens': { - model_id: modelId, - model_text: query, - }, - }, - }, - ], - filter, - }, -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/types.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/types.ts deleted file mode 100644 index a0f549a00ab26..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/elasticsearch_store/helpers/types.ts +++ /dev/null @@ -1,48 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * A hit from the response to an Elasticsearch multi-search request, - * which returns the results of multiple searches in a single request. - * - * Search hits may contain the following properties that may be present in - * knowledge base documents: - * - * 1) the `metadata` property, an object that may have the following properties: - * - `kbResource`: The name of the Knowledge Base resource that the document belongs to, e.g. `esql` - * - `required`: A boolean indicating whether the document is required for searches on the `kbResource` topic - * - `source`: Describes the origin of the document, sometimes a file path via a LangChain DirectoryLoader - * 2) the `text` property, a string containing the text of the document - * 3) the `vector` property, containing the document's embeddings - */ -export interface MsearchKbHit { - _id?: string; - _ignored?: string[]; - _index?: string; - _score?: number; - _source?: { - metadata?: { - kbResource?: string; - required?: boolean; - source?: string; - }; - text?: string; - vector?: { - tokens?: Record; - }; - }; -} - -/** - * A Response from an Elasticsearch multi-search request, which returns the - * results of multiple searches in a single request. - */ -export interface MsearchResponse { - hits?: { - hits?: MsearchKbHit[]; - }; -} diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/embeddings/elasticsearch_embeddings.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/embeddings/elasticsearch_embeddings.ts deleted file mode 100644 index 570f692ecd5ac..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/embeddings/elasticsearch_embeddings.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Embeddings, EmbeddingsParams } from '@langchain/core/embeddings'; -import { Logger } from '@kbn/core/server'; - -/** - * Shell class for Elasticsearch embeddings as not needed in ElasticsearchStore since ELSER embeds on index - */ -export class ElasticsearchEmbeddings extends Embeddings { - private readonly logger: Logger; - constructor(logger: Logger, params?: EmbeddingsParams) { - super(params ?? {}); - this.logger = logger; - } - - /** - * TODO: Use inference API if not re-indexing to create embedding vectors, e.g. - * - * POST _ml/trained_models/.elser_model_2/_infer - * { - * "docs":[{"text_field": "The fool doth think he is wise, but the wise man knows himself to be a fool."}] - * } - */ - - embedDocuments(documents: string[]): Promise { - // Note: implement if/when needed - this.logger.info('ElasticsearchEmbeddings.embedDocuments not implemented'); - return Promise.resolve([]); - } - - embedQuery(_: string): Promise { - // Note: implement if/when needed - this.logger.info('ElasticsearchEmbeddings.embedQuery not implemented'); - return Promise.resolve([]); - } -} diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts index f55006e452cd0..e9d2c1dd2618b 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts @@ -93,8 +93,9 @@ export const callAssistantGraph: AgentExecutor = async ({ const latestMessage = langChainMessages.slice(-1); // the last message - // Check if KB is available - const isEnabledKnowledgeBase = (await dataClients?.kbDataClient?.isModelDeployed()) ?? false; + // Check if KB is available (not feature flag related) + const isEnabledKnowledgeBase = + (await dataClients?.kbDataClient?.isInferenceEndpointExists()) ?? false; // Fetch any applicable tools that the source plugin may have registered const assistantToolParams: AssistantToolParams = { @@ -118,9 +119,8 @@ export const callAssistantGraph: AgentExecutor = async ({ ); // If KB enabled, fetch for any KB IndexEntries and generate a tool for each - if (isEnabledKnowledgeBase && dataClients?.kbDataClient?.isV2KnowledgeBaseEnabled) { + if (isEnabledKnowledgeBase) { const kbTools = await dataClients?.kbDataClient?.getAssistantTools({ - assistantToolParams, esClient, }); if (kbTools) { diff --git a/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.test.ts b/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.test.ts index f03a3394cdaac..5d277abb00667 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.test.ts @@ -86,7 +86,7 @@ const mockContext = { indexTemplateAndPattern: { alias: 'knowledge-base-alias', }, - isModelDeployed: jest.fn().mockResolvedValue(true), + isInferenceEndpointExists: jest.fn().mockResolvedValue(true), }), getAIAssistantAnonymizationFieldsDataClient: jest.fn().mockResolvedValue({ findDocuments: jest.fn().mockResolvedValue(getFindAnonymizationFieldsResultWithSingleHit()), diff --git a/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts b/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts index c6eb81dd86ebd..35b4999a30249 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts @@ -25,9 +25,7 @@ import { buildResponse } from '../../lib/build_response'; import { appendAssistantMessageToConversation, createConversationWithUserInput, - DEFAULT_PLUGIN_NAME, getIsKnowledgeBaseInstalled, - getPluginNameFromRequest, langChainExecute, performChecks, } from '../helpers'; @@ -222,25 +220,14 @@ export const chatCompleteRoute = ( }); } catch (err) { const error = transformError(err as Error); - const pluginName = getPluginNameFromRequest({ - request, - defaultPluginName: DEFAULT_PLUGIN_NAME, - logger, - }); - const v2KnowledgeBaseEnabled = - ctx.elasticAssistant.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault; const kbDataClient = - (await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled, - })) ?? undefined; + (await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient()) ?? undefined; const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient); telemetry?.reportEvent(INVOKE_ASSISTANT_ERROR_EVENT.eventType, { actionTypeId: actionTypeId ?? '', model: request.body.model, errorMessage: error.message, - // TODO rm actionTypeId check when llmClass for bedrock streaming is implemented - // tracked here: https://github.com/elastic/security-team/issues/7363 assistantStreamingEnabled: request.body.isStream ?? false, isEnabledKnowledgeBase: isKnowledgeBaseInstalled, }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts index e4f520b190b5a..4e4b7e5fcd251 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts @@ -33,7 +33,7 @@ import { omit } from 'lodash/fp'; import { buildResponse } from '../../lib/build_response'; import { AssistantDataClients } from '../../lib/langchain/executors/types'; import { AssistantToolParams, ElasticAssistantRequestHandlerContext, GetElser } from '../../types'; -import { DEFAULT_PLUGIN_NAME, isV2KnowledgeBaseEnabled, performChecks } from '../helpers'; +import { DEFAULT_PLUGIN_NAME, performChecks } from '../helpers'; import { fetchLangSmithDataset } from './utils'; import { transformESSearchToAnonymizationFields } from '../../ai_assistant_data_clients/anonymization_fields/helpers'; import { EsAnonymizationFieldsSchema } from '../../ai_assistant_data_clients/anonymization_fields/types'; @@ -91,7 +91,6 @@ export const postEvaluateRoute = ( const actions = ctx.elasticAssistant.actions; const logger = assistantContext.logger.get('evaluate'); const abortSignal = getRequestAbortedSignal(request.events.aborted$); - const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request }); // Perform license, authenticated user and evaluation FF checks const checkResponse = performChecks({ @@ -158,9 +157,7 @@ export const postEvaluateRoute = ( const conversationsDataClient = (await assistantContext.getAIAssistantConversationsDataClient()) ?? undefined; const kbDataClient = - (await assistantContext.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled, - })) ?? undefined; + (await assistantContext.getAIAssistantKnowledgeBaseDataClient()) ?? undefined; const dataClients: AssistantDataClients = { anonymizationFieldsDataClient, conversationsDataClient, @@ -246,7 +243,7 @@ export const postEvaluateRoute = ( // Check if KB is available const isEnabledKnowledgeBase = - (await dataClients.kbDataClient?.isModelDeployed()) ?? false; + (await dataClients.kbDataClient?.isInferenceEndpointExists()) ?? false; // Skeleton request from route to pass to the agents // params will be passed to the actions executor diff --git a/x-pack/plugins/elastic_assistant/server/routes/helpers.ts b/x-pack/plugins/elastic_assistant/server/routes/helpers.ts index 0c5c39f77d692..e68efd8e71f8f 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/helpers.ts @@ -374,8 +374,6 @@ export const langChainExecute = async ({ const assistantTools = assistantContext .getRegisteredTools(pluginName) .filter((x) => x.id !== 'attack-discovery'); // We don't (yet) support asking the assistant for NEW attack discoveries from a conversation - const v2KnowledgeBaseEnabled = - assistantContext.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault; // get a scoped esClient for assistant memory const esClient = context.core.elasticsearch.client.asCurrentUser; @@ -389,9 +387,7 @@ export const langChainExecute = async ({ // Create an ElasticsearchStore for KB interactions const kbDataClient = - (await assistantContext.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled, - })) ?? undefined; + (await assistantContext.getAIAssistantKnowledgeBaseDataClient()) ?? undefined; const dataClients: AssistantDataClients = { anonymizationFieldsDataClient: anonymizationFieldsDataClient ?? undefined, @@ -643,29 +639,6 @@ export const performChecks = ({ }; }; -/** - * Returns whether the v2 KB is enabled - * - * @param context - Route context - * @param request - Route KibanaRequest - - */ -export const isV2KnowledgeBaseEnabled = ({ - context, - request, -}: { - context: AwaitedProperties< - Pick - >; - request: KibanaRequest; -}): boolean => { - const pluginName = getPluginNameFromRequest({ - request, - defaultPluginName: DEFAULT_PLUGIN_NAME, - }); - return context.elasticAssistant.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault; -}; - /** * Telemetry function to determine whether knowledge base has been installed * @param kbDataClient @@ -674,11 +647,11 @@ export const getIsKnowledgeBaseInstalled = async ( kbDataClient?: AIAssistantKnowledgeBaseDataClient | null ): Promise => { let securityLabsDocsExist = false; - let isModelDeployed = false; + let isInferenceEndpointExists = false; if (kbDataClient != null) { try { - isModelDeployed = await kbDataClient.isModelDeployed(); - if (isModelDeployed) { + isInferenceEndpointExists = await kbDataClient.isInferenceEndpointExists(); + if (isInferenceEndpointExists) { securityLabsDocsExist = ( await kbDataClient.getKnowledgeBaseDocumentEntries({ @@ -692,5 +665,5 @@ export const getIsKnowledgeBaseInstalled = async ( } } - return isModelDeployed && securityLabsDocsExist; + return isInferenceEndpointExists && securityLabsDocsExist; }; diff --git a/x-pack/plugins/elastic_assistant/server/routes/index.ts b/x-pack/plugins/elastic_assistant/server/routes/index.ts index 928c3211faa9b..c30a62872a82d 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/index.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/index.ts @@ -13,7 +13,6 @@ export { postAttackDiscoveryRoute } from './attack_discovery/post/post_attack_di export { getAttackDiscoveryRoute } from './attack_discovery/get/get_attack_discovery'; // Knowledge Base -export { deleteKnowledgeBaseRoute } from './knowledge_base/delete_knowledge_base'; export { getKnowledgeBaseIndicesRoute } from './knowledge_base/get_knowledge_base_indices'; export { getKnowledgeBaseStatusRoute } from './knowledge_base/get_knowledge_base_status'; export { postKnowledgeBaseRoute } from './knowledge_base/post_knowledge_base'; diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts index 052b2cac57609..1c26c6d77b53f 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/constants.ts @@ -5,8 +5,6 @@ * 2.0. */ -export const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-kb'; -export const KNOWLEDGE_BASE_INGEST_PIPELINE = '.kibana-elastic-ai-assistant-kb-ingest-pipeline'; // Query for determining if ESQL docs have been loaded, searches for a specific doc. Intended for the ElasticsearchStore.similaritySearch() // Note: We may want to add a tag of the resource name to the document metadata, so we can CRUD by specific resource export const ESQL_DOCS_LOADED_QUERY = diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/delete_knowledge_base.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/delete_knowledge_base.ts deleted file mode 100644 index 3e387e8a8a4d2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/delete_knowledge_base.ts +++ /dev/null @@ -1,83 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IRouter, KibanaRequest } from '@kbn/core/server'; -import { transformError } from '@kbn/securitysolution-es-utils'; - -import { - ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION, - ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL, -} from '@kbn/elastic-assistant-common'; -import { - DeleteKnowledgeBaseRequestParams, - DeleteKnowledgeBaseResponse, -} from '@kbn/elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen'; -import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common'; -import { buildResponse } from '../../lib/build_response'; -import { ElasticAssistantRequestHandlerContext } from '../../types'; -import { isV2KnowledgeBaseEnabled } from '../helpers'; - -/** - * Delete Knowledge Base index, pipeline, and resources (collection of documents) - * @param router - */ -export const deleteKnowledgeBaseRoute = ( - router: IRouter -) => { - router.versioned - .delete({ - access: 'internal', - path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL, - options: { - tags: ['access:elasticAssistant'], - }, - }) - .addVersion( - { - version: ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION, - validate: { - request: { - params: buildRouteValidationWithZod(DeleteKnowledgeBaseRequestParams), - }, - }, - }, - async (context, request: KibanaRequest, response) => { - const resp = buildResponse(response); - const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); - const assistantContext = ctx.elasticAssistant; - const logger = ctx.elasticAssistant.logger; - - // FF Check for V2 KB - const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request }); - - try { - const knowledgeBaseDataClient = - await assistantContext.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled, - }); - if (!knowledgeBaseDataClient) { - return response.custom({ body: { success: false }, statusCode: 500 }); - } - - // TODO: This delete API is likely not needed and can be replaced by the new `entries` API - const body: DeleteKnowledgeBaseResponse = { - success: false, - }; - - return response.ok({ body }); - } catch (err) { - logger.error(err); - const error = transformError(err); - - return resp.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } - ); -}; diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts index fc49068a09cc9..c6c5f9d94bef3 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts @@ -155,7 +155,6 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug // Perform license, authenticated user and FF checks const checkResponse = performChecks({ - capability: 'assistantKnowledgeBaseByDefault', context: ctx, request, response, @@ -187,9 +186,7 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug // subscribing to completed$, because it handles both cases when request was completed and aborted. // when route is finished by timeout, aborted$ is not getting fired request.events.completed$.subscribe(() => abortController.abort()); - const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled: true, - }); + const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient(); const spaceId = ctx.elasticAssistant.getSpaceId(); const authenticatedUser = checkResponse.currentUser; const userFilter = getKBUserFilter(authenticatedUser); @@ -288,8 +285,7 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug global: entry.users != null && entry.users.length === 0, }) ), - getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) => - getUpdateScript({ entry, isPatch: true }), + getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) => getUpdateScript({ entry }), authenticatedUser, }); const created = diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts index d5df2d02055fd..4c1ea3851aaf5 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts @@ -47,7 +47,6 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout // Perform license, authenticated user and FF checks const checkResponse = performChecks({ - capability: 'assistantKnowledgeBaseByDefault', context: ctx, request, response, @@ -56,10 +55,7 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout return checkResponse.response; } - // Check mappings and upgrade if necessary -- this route only supports v2 KB, so always `true` - const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled: true, - }); + const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient(); logger.debug(() => `Creating KB Entry:\n${JSON.stringify(request.body)}`); const createResponse = await kbDataClient?.createKnowledgeBaseEntry({ diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts index 13334d0d829b1..e4035264a8352 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts @@ -58,7 +58,6 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout // Perform license, authenticated user and FF checks const checkResponse = performChecks({ - capability: 'assistantKnowledgeBaseByDefault', context: ctx, request, response, @@ -67,9 +66,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout return checkResponse.response; } - const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled: true, - }); + const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient(); const currentUser = checkResponse.currentUser; const userFilter = getKBUserFilter(currentUser); const systemFilter = ` AND (kb_resource:"user" OR type:"index")`; diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts index b30e5ac3653ad..a31af7596977a 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts @@ -35,7 +35,7 @@ describe('Get Knowledge Base Status Route', () => { }, isModelInstalled: jest.fn().mockResolvedValue(true), isSetupAvailable: jest.fn().mockResolvedValue(true), - isModelDeployed: jest.fn().mockResolvedValue(true), + isInferenceEndpointExists: jest.fn().mockResolvedValue(true), isSetupInProgress: false, isSecurityLabsDocsLoaded: jest.fn().mockResolvedValue(true), isUserDataExists: jest.fn().mockResolvedValue(true), diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts index f278cd469ac0e..4e8112b420d06 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts @@ -17,7 +17,6 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/ import { KibanaRequest } from '@kbn/core/server'; import { buildResponse } from '../../lib/build_response'; import { ElasticAssistantPluginRouter } from '../../types'; -import { isV2KnowledgeBaseEnabled } from '../helpers'; /** * Get the status of the Knowledge Base index, pipeline, and resources (collection of documents) @@ -49,12 +48,7 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter const logger = ctx.elasticAssistant.logger; try { - // FF Check for V2 KB - const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request }); - - const kbDataClient = await assistantContext.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled, - }); + const kbDataClient = await assistantContext.getAIAssistantKnowledgeBaseDataClient(); if (!kbDataClient) { return response.custom({ body: { success: false }, statusCode: 500 }); } @@ -63,7 +57,7 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter const pipelineExists = true; // Installed at startup, always true const modelExists = await kbDataClient.isModelInstalled(); const setupAvailable = await kbDataClient.isSetupAvailable(); - const isModelDeployed = await kbDataClient.isModelDeployed(); + const isInferenceEndpointExists = await kbDataClient.isInferenceEndpointExists(); const body: ReadKnowledgeBaseResponse = { elser_exists: modelExists, @@ -73,13 +67,9 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter pipeline_exists: pipelineExists, }; - if (indexExists && isModelDeployed) { - const securityLabsExists = v2KnowledgeBaseEnabled - ? await kbDataClient.isSecurityLabsDocsLoaded() - : true; - const userDataExists = v2KnowledgeBaseEnabled - ? await kbDataClient.isUserDataExists() - : true; + if (indexExists && isInferenceEndpointExists) { + const securityLabsExists = await kbDataClient.isSecurityLabsDocsLoaded(); + const userDataExists = await kbDataClient.isUserDataExists(); return response.ok({ body: { diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts index 23604886e4a52..fa7716a51033d 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.ts @@ -16,7 +16,6 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/ import { IKibanaResponse } from '@kbn/core/server'; import { buildResponse } from '../../lib/build_response'; import { ElasticAssistantPluginRouter } from '../../types'; -import { isV2KnowledgeBaseEnabled } from '../helpers'; // Since we're awaiting on ELSER setup, this could take a bit (especially if ML needs to autoscale) // Consider just returning if attempt was successful, and switch to client polling @@ -54,19 +53,12 @@ export const postKnowledgeBaseRoute = (router: ElasticAssistantPluginRouter) => const assistantContext = ctx.elasticAssistant; const core = ctx.core; const soClient = core.savedObjects.getClient(); - - // FF Check for V2 KB - const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request }); - // Only allow modelId override if FF is enabled as this will re-write the ingest pipeline and break any previous KB entries - // This is only really needed for API integration tests - const modelIdOverride = v2KnowledgeBaseEnabled ? request.query.modelId : undefined; const ignoreSecurityLabs = request.query.ignoreSecurityLabs; try { const knowledgeBaseDataClient = await assistantContext.getAIAssistantKnowledgeBaseDataClient({ - modelIdOverride, - v2KnowledgeBaseEnabled, + modelIdOverride: request.query.modelId, }); if (!knowledgeBaseDataClient) { return response.custom({ body: { success: false }, statusCode: 500 }); @@ -74,7 +66,6 @@ export const postKnowledgeBaseRoute = (router: ElasticAssistantPluginRouter) => await knowledgeBaseDataClient.setupKnowledgeBase({ soClient, - v2KnowledgeBaseEnabled, ignoreSecurityLabs, }); diff --git a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts index bb217f7f5aa3a..43264a6c1f54b 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts @@ -23,9 +23,7 @@ import { buildResponse } from '../lib/build_response'; import { ElasticAssistantRequestHandlerContext, GetElser } from '../types'; import { appendAssistantMessageToConversation, - DEFAULT_PLUGIN_NAME, getIsKnowledgeBaseInstalled, - getPluginNameFromRequest, getSystemPromptFromUserConversation, langChainExecute, performChecks, @@ -159,17 +157,9 @@ export const postActionsConnectorExecuteRoute = ( if (onLlmResponse) { await onLlmResponse(error.message, {}, true); } - const pluginName = getPluginNameFromRequest({ - request, - defaultPluginName: DEFAULT_PLUGIN_NAME, - logger, - }); - const v2KnowledgeBaseEnabled = - assistantContext.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault; + const kbDataClient = - (await assistantContext.getAIAssistantKnowledgeBaseDataClient({ - v2KnowledgeBaseEnabled, - })) ?? undefined; + (await assistantContext.getAIAssistantKnowledgeBaseDataClient()) ?? undefined; const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient); telemetry.reportEvent(INVOKE_ASSISTANT_ERROR_EVENT.eventType, { actionTypeId: request.body.actionTypeId, diff --git a/x-pack/plugins/elastic_assistant/server/routes/request_context_factory.ts b/x-pack/plugins/elastic_assistant/server/routes/request_context_factory.ts index 7d97029e7252a..3f81763db49d9 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/request_context_factory.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/request_context_factory.ts @@ -83,33 +83,28 @@ export class RequestContextFactory implements IRequestContextFactory { telemetry: core.analytics, - // Note: Due to plugin lifecycle and feature flag registration timing, we need to pass in the feature flag here - // Remove `v2KnowledgeBaseEnabled` once 'assistantKnowledgeBaseByDefault' feature flag is removed - // Additionally, modelIdOverride is used here to enable setting up the KB using a different ELSER model, which + // Note: modelIdOverride is used here to enable setting up the KB using a different ELSER model, which // is necessary for testing purposes (`pt_tiny_elser`). - getAIAssistantKnowledgeBaseDataClient: memoize( - async ({ modelIdOverride, v2KnowledgeBaseEnabled = false }) => { - const currentUser = getCurrentUser(); - - const { securitySolutionAssistant } = await coreStart.capabilities.resolveCapabilities( - request, - { - capabilityPath: 'securitySolutionAssistant.*', - } - ); - - return this.assistantService.createAIAssistantKnowledgeBaseDataClient({ - spaceId: getSpaceId(), - logger: this.logger, - licensing: context.licensing, - currentUser, - modelIdOverride, - v2KnowledgeBaseEnabled, - manageGlobalKnowledgeBaseAIAssistant: - securitySolutionAssistant.manageGlobalKnowledgeBaseAIAssistant as boolean, - }); - } - ), + getAIAssistantKnowledgeBaseDataClient: memoize(async (params) => { + const currentUser = getCurrentUser(); + + const { securitySolutionAssistant } = await coreStart.capabilities.resolveCapabilities( + request, + { + capabilityPath: 'securitySolutionAssistant.*', + } + ); + + return this.assistantService.createAIAssistantKnowledgeBaseDataClient({ + spaceId: getSpaceId(), + logger: this.logger, + licensing: context.licensing, + currentUser, + modelIdOverride: params?.modelIdOverride, + manageGlobalKnowledgeBaseAIAssistant: + securitySolutionAssistant.manageGlobalKnowledgeBaseAIAssistant as boolean, + }); + }), getAttackDiscoveryDataClient: memoize(() => { const currentUser = getCurrentUser(); diff --git a/x-pack/plugins/elastic_assistant/server/types.ts b/x-pack/plugins/elastic_assistant/server/types.ts index 00fec0dcabc6d..b021ef5a7017d 100755 --- a/x-pack/plugins/elastic_assistant/server/types.ts +++ b/x-pack/plugins/elastic_assistant/server/types.ts @@ -126,7 +126,7 @@ export interface ElasticAssistantApiRequestHandlerContext { getCurrentUser: () => AuthenticatedUser | null; getAIAssistantConversationsDataClient: () => Promise; getAIAssistantKnowledgeBaseDataClient: ( - params: GetAIAssistantKnowledgeBaseDataClientParams + params?: GetAIAssistantKnowledgeBaseDataClientParams ) => Promise; getAttackDiscoveryDataClient: () => Promise; getAIAssistantPromptsDataClient: () => Promise; diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/connect/generate_api_key_modal/generate_search_application_api_key_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/connect/generate_api_key_modal/generate_search_application_api_key_modal.test.tsx index 1ebd426b8b9c1..1f23866261b5f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/connect/generate_api_key_modal/generate_search_application_api_key_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/connect/generate_api_key_modal/generate_search_application_api_key_modal.test.tsx @@ -9,7 +9,7 @@ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea_logi import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { shallow } from 'enzyme'; import { EuiModal, EuiFieldText, EuiCodeBlock } from '@elastic/eui'; @@ -17,6 +17,8 @@ const mockActions = { makeRequest: jest.fn(), setKeyName: jest.fn() }; const mockValues = { apiKey: '', isLoading: false, isSuccess: false, keyName: '' }; +import { mountWithIntl } from '@kbn/test-jest-helpers'; + import { GenerateSearchApplicationApiKeyModal } from './generate_search_application_api_key_modal'; const onCloseMock = jest.fn(); @@ -50,7 +52,7 @@ describe('GenerateSearchApplicationApiKeyModal', () => { }); it('pre-set the key name with search application name', () => { - mount( + mountWithIntl( = ({ onClose, searchApplicationName }) => { const { keyName, apiKey, isLoading, isSuccess } = useValues(GenerateApiKeyModalLogic); const { setKeyName } = useActions(GenerateApiKeyModalLogic); + const modalTitleId = useGeneratedHtmlId(); const { makeRequest } = useActions(GenerateSearchApplicationApiKeyLogic); + const copyApiKeyRef = useRef(null); useEffect(() => { setKeyName(`${searchApplicationName} read-only API key`); }, [searchApplicationName]); + useEffect(() => { + if (isSuccess) { + copyApiKeyRef.current?.focus(); + } + }, [isSuccess]); + return ( - + - + {i18n.translate( 'xpack.enterpriseSearch.searchApplication.searchApplication.api.generateApiKeyModal.title', { @@ -65,15 +74,24 @@ export const GenerateSearchApplicationApiKeyModal: React.FC< <> - + {!isSuccess ? ( <> - + + } + fullWidth + > ) : ( - {keyName} - + {keyName}, + }} + /> + } + color="success" + iconType="check" + role="alert" + /> {apiKey ? ( ) : ( diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts index dbaf1205cdf98..6679140314cb5 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { EntityClient, EnitityInstance } from './entity_client'; +import { EntityClient } from './entity_client'; import { coreMock } from '@kbn/core/public/mocks'; +import type { EntityInstance } from '@kbn/entities-schema'; -const commonEntityFields: EnitityInstance = { +const commonEntityFields: EntityInstance = { entity: { last_seen_timestamp: '2023-10-09T00:00:00Z', id: '1', display_name: 'entity_name', definition_id: 'entity_definition_id', - } as EnitityInstance['entity'], + } as EntityInstance['entity'], }; describe('EntityClient', () => { @@ -26,7 +27,7 @@ describe('EntityClient', () => { describe('asKqlFilter', () => { it('should return the kql filter', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -42,7 +43,7 @@ describe('EntityClient', () => { }); it('should return the kql filter when indentity_fields is composed by multiple fields', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -59,7 +60,7 @@ describe('EntityClient', () => { }); it('should ignore fields that are not present in the entity', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['host.name', 'foo.bar'], @@ -76,7 +77,7 @@ describe('EntityClient', () => { describe('getIdentityFieldsValue', () => { it('should return identity fields values', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -93,7 +94,7 @@ describe('EntityClient', () => { }); it('should return identity fields values when indentity_fields is composed by multiple fields', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -112,7 +113,7 @@ describe('EntityClient', () => { }); it('should return identity fields when field is in the root', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['name'], @@ -127,7 +128,7 @@ describe('EntityClient', () => { }); it('should throw an error when identity fields are missing', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { ...commonEntityFields, }; diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index 08794873ba930..7132dc50330d5 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { z } from '@kbn/zod'; import { CoreSetup, CoreStart } from '@kbn/core/public'; import { ClientRequestParamsOf, @@ -14,7 +13,7 @@ import { isHttpFetchError, } from '@kbn/server-route-repository-client'; import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; -import { entityLatestSchema } from '@kbn/entities-schema'; +import type { EntityInstance, EntityMetadata } from '@kbn/entities-schema'; import { castArray } from 'lodash'; import { DisableManagedEntityResponse, @@ -39,8 +38,6 @@ type CreateEntityDefinitionQuery = QueryParamOf< ClientRequestParamsOf >; -export type EnitityInstance = z.infer; - export class EntityClient { public readonly repositoryClient: EntityManagerRepositoryClient['fetch']; @@ -90,8 +87,12 @@ export class EntityClient { } } - asKqlFilter(entityLatest: EnitityInstance) { - const identityFieldsValue = this.getIdentityFieldsValue(entityLatest); + asKqlFilter( + entityInstance: { + entity: Pick; + } & Required + ) { + const identityFieldsValue = this.getIdentityFieldsValue(entityInstance); const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => { return nodeTypes.function.buildNode('is', identityField, value); @@ -104,8 +105,12 @@ export class EntityClient { return toKqlExpression(kqlExpression); } - getIdentityFieldsValue(entityLatest: EnitityInstance) { - const { identity_fields: identityFields } = entityLatest.entity; + getIdentityFieldsValue( + entityInstance: { + entity: Pick; + } & Required + ) { + const { identity_fields: identityFields } = entityInstance.entity; if (!identityFields) { throw new Error('Identity fields are missing'); @@ -114,7 +119,7 @@ export class EntityClient { return castArray(identityFields).reduce((acc, field) => { const value = field.split('.').reduce((obj: any, part: string) => { return obj && typeof obj === 'object' ? (obj as Record)[part] : undefined; - }, entityLatest); + }, entityInstance); if (value) { acc[field] = value; diff --git a/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts b/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts index 5e518618d98d8..422c16a726843 100644 --- a/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts +++ b/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts @@ -24,6 +24,12 @@ export const initFindFieldsMetadataRoute = ({ .addVersion( { version: '1', + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, validate: { request: { query: createValidationFunction(fieldsMetadataV1.findFieldsMetadataRequestQueryRT), diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 7dccb7ba1dfe0..da40f8ff7d819 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -240,6 +240,7 @@ class AppContextService { // soClient as kibana internal users, be careful on how you use it, security is not enabled return appContextService.getSavedObjects().getScopedClient(fakeRequest, { excludedExtensions: [SECURITY_EXTENSION_ID, SPACES_EXTENSION_ID], + includedHiddenTypes: [UNINSTALL_TOKENS_SAVED_OBJECT_TYPE], }); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.test.ts index 255572d57cf49..1a4f2998d49dd 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.test.ts @@ -42,6 +42,30 @@ const mockedRemoveArchiveEntries = removeArchiveEntries as jest.MockedFunction< let soClient: jest.Mocked; let esClient: jest.Mocked; +const assetsMap = new Map([ + [ + 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json', + Buffer.from('{"content": "data"}'), + ], + ['security_detection_engine-8.16.1/LICENSE.txt', Buffer.from('{"content": "data"}')], + ['security_detection_engine-8.16.1/NOTICE.txt', Buffer.from('{"content": "data"}')], + ['security_detection_engine-8.16.1/changelog.yml', Buffer.from('{"content": "data"}')], + ['security_detection_engine-8.16.1/manifest.yml', Buffer.from('{"content": "data"}')], + ['security_detection_engine-8.16.1/docs/README.md', Buffer.from('{"content": "data"}')], + [ + 'security_detection_engine-8.16.1/img/security-logo-color-64px.svg', + Buffer.from('{"content": "data"}'), + ], + [ + 'security_detection_engine-8.16.1/kibana/security_rule/000047bb-b27a-47ec-8b62-ef1a5d2c9e19_208.json', + Buffer.from('{"content": "data"}'), + ], + [ + 'security_detection_engine-8.16.1/kibana/security_rule/000047bb-b27a-47ec-8b62-ef1a5d2c9e19_209.json', + Buffer.from('{"content": "data"}'), + ], +]); + const packageInstallContext = { packageInfo: { title: 'title', @@ -56,13 +80,8 @@ const packageInstallContext = { owner: { github: 'elastic/fleet' }, } as any, paths: ['some/path/1', 'some/path/2'], - assetsMap: new Map([ - [ - 'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json', - Buffer.from('{"content": "data"}'), - ], - ]), - archiveIterator: createArchiveIteratorFromMap(new Map()), + assetsMap, + archiveIterator: createArchiveIteratorFromMap(assetsMap), }; const getMockInstalledPackageSo = ( installedEs: EsAssetReference[] = [] @@ -196,6 +215,63 @@ describe('stepSaveArchiveEntries', () => { ], }); }); + + it('should save package icons, readme, and changelog but not Kibana assets with useStreaming:true ', async () => { + jest.mocked(mockedSaveArchiveEntriesFromAssetsMap).mockResolvedValue({ + saved_objects: [ + { + id: 'test', + attributes: { + package_name: 'test-package', + package_version: '1.0.0', + install_source: 'registry', + asset_path: 'some/path', + media_type: '', + data_utf8: '', + data_base64: '', + }, + type: '', + references: [], + }, + ], + }); + await stepSaveArchiveEntries({ + savedObjectsClient: soClient, + // @ts-ignore + savedObjectsImporter: jest.fn(), + esClient, + logger: loggerMock.create(), + packageInstallContext, + installedPkg, + installType: 'update', + installSource: 'registry', + spaceId: DEFAULT_SPACE_ID, + useStreaming: true, + esReferences: [ + { + id: 'something', + type: ElasticsearchAssetType.ilmPolicy, + }, + ], + }); + expect( + [ + ...(jest + .mocked(mockedSaveArchiveEntriesFromAssetsMap) + .mock.lastCall?.[0].assetsMap?.keys() ?? []), + ].sort() + ).toMatchInlineSnapshot(` + Array [ + "endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json", + "security_detection_engine-8.16.1/LICENSE.txt", + "security_detection_engine-8.16.1/NOTICE.txt", + "security_detection_engine-8.16.1/changelog.yml", + "security_detection_engine-8.16.1/docs/README.md", + "security_detection_engine-8.16.1/img/security-logo-color-64px.svg", + "security_detection_engine-8.16.1/manifest.yml", + ] + `); + }); }); describe('cleanupArchiveEntriesStep', () => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.ts index 7db44bb243f85..f081d9a93e633 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_save_archive_entries.ts @@ -14,7 +14,7 @@ import { withPackageSpan } from '../../utils'; import type { InstallContext } from '../_state_machine_package_install'; import { INSTALL_STATES } from '../../../../../../common/types'; -import { MANIFEST_NAME } from '../../../archive/parse'; +import { isKibanaAssetType } from '../../../kibana/assets/install'; export async function stepSaveArchiveEntries(context: InstallContext) { const { packageInstallContext, savedObjectsClient, installSource, useStreaming } = context; @@ -28,7 +28,8 @@ export async function stepSaveArchiveEntries(context: InstallContext) { if (useStreaming) { assetsMap = new Map(); await archiveIterator.traverseEntries(async (entry) => { - if (entry.path.endsWith(MANIFEST_NAME)) { + // Skip only kibana assets type + if (!isKibanaAssetType(entry.path)) { assetsMap.set(entry.path, entry.buffer); } }); diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts index 0a44a3df0f294..1f58fcafd396b 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts @@ -46,7 +46,7 @@ import { appContextService } from '../../app_context'; import { agentPolicyService, getAgentPolicySavedObjectType } from '../../agent_policy'; import { isSpaceAwarenessEnabled } from '../../spaces/helpers'; -interface UninstallTokenSOAttributes { +export interface UninstallTokenSOAttributes { policy_id: string; token: string; token_plain: string; diff --git a/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts index ab69d4708c436..079148a6ae041 100644 --- a/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/spaces/agent_policy.test.ts @@ -39,6 +39,22 @@ describe('updateAgentPolicySpaces', () => { jest .mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension()) .updateObjectsSpaces.mockResolvedValue({ objects: [] }); + + jest + .mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension()) + .find.mockResolvedValue({ + total: 1, + page: 1, + per_page: 100, + saved_objects: [ + { + id: 'token1', + attributes: { + namespaces: ['default'], + }, + } as any, + ], + }); }); it('does nothings if agent policy already in correct space', async () => { @@ -87,6 +103,18 @@ describe('updateAgentPolicySpaces', () => { ['default'], { namespace: 'default', refresh: 'wait_for' } ); + + expect( + jest.mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension()).bulkUpdate + ).toBeCalledWith([ + { + id: 'token1', + type: 'fleet-uninstall-tokens', + attributes: { + namespaces: ['test'], + }, + }, + ]); }); it('throw when trying to change space to a policy with reusable package policies', async () => { diff --git a/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts b/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts index 2f8d5ff1b14c7..e123ca4426654 100644 --- a/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts @@ -13,6 +13,8 @@ import { AGENTS_INDEX, AGENT_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, + SO_SEARCH_LIMIT, + UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, } from '../../../common/constants'; import { appContextService } from '../app_context'; @@ -22,6 +24,7 @@ import { packagePolicyService } from '../package_policy'; import { FleetError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { isSpaceAwarenessEnabled } from './helpers'; +import type { UninstallTokenSOAttributes } from '../security/uninstall_token_service'; export async function updateAgentPolicySpaces({ agentPolicyId, @@ -112,6 +115,25 @@ export async function updateAgentPolicySpaces({ } } + // Update uninstall tokens + const uninstallTokensRes = await soClient.find({ + perPage: SO_SEARCH_LIMIT, + type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, + filter: `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.attributes.policy_id:"${agentPolicyId}"`, + }); + + if (uninstallTokensRes.total > 0) { + await soClient.bulkUpdate( + uninstallTokensRes.saved_objects.map((so) => ({ + id: so.id, + type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, + attributes: { + namespaces: newSpaceIds, + }, + })) + ); + } + // Update fleet server index agents, enrollment api keys await esClient.updateByQuery({ index: ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 7d41e326372e5..ad53002a1b0cb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -5,20 +5,12 @@ * 2.0. */ -import React, { useEffect } from 'react'; -import { ReactWrapper } from 'enzyme'; +import React from 'react'; import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { EditorFrame, EditorFrameProps } from './editor_frame'; -import { - DatasourceMap, - DatasourcePublicAPI, - DatasourceSuggestion, - Visualization, - VisualizationMap, -} from '../../types'; -import { act } from '@testing-library/react'; +import { DatasourceMap, DatasourcePublicAPI, Visualization, VisualizationMap } from '../../types'; import { coreMock } from '@kbn/core/public/mocks'; import { createMockVisualization, @@ -29,30 +21,16 @@ import { renderWithReduxStore, } from '../../mocks'; import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; -import { Droppable, useDragDropContext } from '@kbn/dom-drag-drop'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks'; -import { mockDataPlugin, mountWithProvider } from '../../mocks'; +import { mockDataPlugin } from '../../mocks'; import { LensAppState, setState } from '../../state_management'; import { getLensInspectorService } from '../../lens_inspector_service'; import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; -function generateSuggestion(state = {}): DatasourceSuggestion { - return { - state, - table: { - columns: [], - isMultiRow: true, - layerId: 'first', - changeType: 'unchanged', - }, - keptLayerIds: ['first'], - }; -} - function wrapDataViewsContract() { const dataViewsContract = dataViewPluginMocks.createStartContract(); return { @@ -437,180 +415,5 @@ describe('editor_frame', () => { }) ); }); - describe('legacy tests', () => { - let instance: ReactWrapper; - - afterEach(() => { - instance.unmount(); - }); - - it('should use the currently selected visualization if possible on field drop', async () => { - mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']); - const suggestionVisState = {}; - const props = { - ...getDefaultProps(), - visualizationMap: { - testVis: { - ...mockVisualization, - getSuggestions: () => [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.6, - state: suggestionVisState, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], - }, - testVis2: { - ...mockVisualization2, - getSuggestions: () => [ - { - score: 0.8, - state: {}, - title: 'Suggestion3', - previewIcon: 'empty', - }, - ], - }, - }, - datasourceMap: { - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - DataPanelComponent: jest.fn().mockImplementation(() =>
), - }, - }, - } as EditorFrameProps; - instance = ( - await mountWithProvider(, { - preloadedState: { - datasourceStates: { - testDatasource: { - isLoading: false, - state: { - internalState1: '', - }, - }, - }, - }, - }) - ).instance; - - instance.update(); - - act(() => { - instance.find('[data-test-subj="mockVisA"]').find(Droppable).prop('onDrop')!( - { - indexPatternId: '1', - field: {}, - id: '1', - humanData: { label: 'draggedField' }, - }, - 'field_add' - ); - }); - - expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( - expect.objectContaining({ - state: suggestionVisState, - }) - ); - }); - - it('should use the highest priority suggestion available', async () => { - mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']); - const suggestionVisState = {}; - const mockVisualization3 = { - ...createMockVisualization('testVis3', ['third']), - getSuggestions: () => [ - { - score: 0.9, - state: suggestionVisState, - title: 'Suggestion3', - previewIcon: 'empty', - }, - { - score: 0.7, - state: {}, - title: 'Suggestion4', - previewIcon: 'empty', - }, - ], - }; - - const props = { - ...getDefaultProps(), - visualizationMap: { - testVis: { - ...mockVisualization, - // do not return suggestions for the currently active vis, otherwise it will be chosen - getSuggestions: () => [], - }, - testVis2: { - ...mockVisualization2, - getSuggestions: () => [], - }, - testVis3: { - ...mockVisualization3, - }, - }, - datasourceMap: { - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - DataPanelComponent: jest.fn().mockImplementation(() => { - const [, dndDispatch] = useDragDropContext(); - useEffect(() => { - dndDispatch({ - type: 'startDragging', - payload: { - dragging: { - id: 'draggedField', - humanData: { label: '1' }, - }, - }, - }); - }, [dndDispatch]); - return
; - }), - }, - }, - } as EditorFrameProps; - - instance = (await mountWithProvider()).instance; - - instance.update(); - - act(() => { - instance.find(Droppable).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!( - { - indexPatternId: '1', - field: {}, - id: '1', - humanData: { - label: 'label', - }, - }, - 'field_add' - ); - }); - - expect(mockVisualization3.getConfiguration).toHaveBeenCalledWith( - expect.objectContaining({ - state: suggestionVisState, - }) - ); - }); - }); }); }); diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index 89bb4b800eb91..1f81a94227611 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -930,7 +930,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => { items.push( @@ -946,7 +946,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => { items.push( { closePopover(); const additionalField = getAdditionalField(anomaly); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index d09791941a379..96692ab073738 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -97,42 +97,45 @@ export const SplitCards: FC> = memo( } return ( - - - {(fieldValues.length === 0 || numberOfDetectors === 0) && <>{children}} - {fieldValues.length > 0 && numberOfDetectors > 0 && splitField !== null && ( - - {(jobType === JOB_TYPE.MULTI_METRIC || jobType === JOB_TYPE.GEO) && ( - + <> + + + {(fieldValues.length === 0 || numberOfDetectors === 0) && <>{children}} + {fieldValues.length > 0 && numberOfDetectors > 0 && splitField !== null && ( + + {(jobType === JOB_TYPE.MULTI_METRIC || jobType === JOB_TYPE.GEO) && ( + +
+ +
+ +
+ )} + + {getBackPanels()} +
- + {fieldValues[0]}
- -
- )} - - {getBackPanels()} - -
- {fieldValues[0]} -
- - <>{children} -
-
- )} -
-
+ + <>{children} + + + )} + + + {splitField !== null ? : null} + ); } ); diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts index 730e9c443854e..af23fc8a2ad7e 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts @@ -115,7 +115,10 @@ describe.skip('Transaction details', () => { ); cy.contains('Top 5 errors', { timeout: 30000 }); - cy.getByTestSubj('topErrorsForTransactionTable').contains('a', '[MockError] Foo').click(); + cy.getByTestSubj('topErrorsForTransactionTable') + .should('be.visible') + .contains('a', '[MockError] Foo', { timeout: 10000 }) + .click(); cy.url().should('include', 'opbeans-java/errors'); }); diff --git a/x-pack/plugins/observability_solution/infra/common/ui_settings.ts b/x-pack/plugins/observability_solution/infra/common/ui_settings.ts index 95f1ee0a44bae..9b85630761942 100644 --- a/x-pack/plugins/observability_solution/infra/common/ui_settings.ts +++ b/x-pack/plugins/observability_solution/infra/common/ui_settings.ts @@ -23,6 +23,13 @@ export const uiSettings: Record = { description: i18n.translate('xpack.infra.enableLogsStreamDescription', { defaultMessage: 'Enables the legacy Logs Stream application and dashboard panel. ', }), + deprecation: { + message: i18n.translate('xpack.infra.enableLogsStreamDeprecationWarning', { + defaultMessage: + 'Logs Stream is deprecated, and this setting will be removed in Kibana 9.0.', + }), + docLinksKey: 'generalSettings', + }, type: 'boolean', schema: schema.boolean(), requiresPageReload: true, diff --git a/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx b/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx index 21e61c08d281b..0edb8b9ab2924 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/logs_deprecation_callout.tsx @@ -9,36 +9,28 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton } from '@elastic/eui'; -import { - AllDatasetsLocatorParams, - ALL_DATASETS_LOCATOR_ID, - DatasetLocatorParams, -} from '@kbn/deeplinks-observability'; import { getRouterLinkProps } from '@kbn/router-utils'; import useLocalStorage from 'react-use/lib/useLocalStorage'; - import { euiThemeVars } from '@kbn/ui-theme'; import { css } from '@emotion/css'; import { LocatorPublic } from '@kbn/share-plugin/common'; +import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { useKibanaContextForPlugin } from '../hooks/use_kibana'; const pageConfigurations = { stream: { dismissalStorageKey: 'log_stream_deprecation_callout_dismissed', - message: i18n.translate('xpack.infra.logsDeprecationCallout.p.theNewLogsExplorerLabel', { + message: i18n.translate('xpack.infra.logsDeprecationCallout.stream.exploreWithDiscover', { defaultMessage: - 'The new Logs Explorer makes viewing and inspecting your logs easier with more features, better performance, and more intuitive navigation. We recommend switching to Logs Explorer, as it will replace Logs Stream in a future version.', + 'Logs Stream and Logs Explorer are set to be deprecated. Switch to Discover which now includes their functionality plus more features, better performance, and more intuitive navigation. ', }), }, settings: { dismissalStorageKey: 'log_settings_deprecation_callout_dismissed', - message: i18n.translate( - 'xpack.infra.logsSettingsDeprecationCallout.p.theNewLogsExplorerLabel', - { - defaultMessage: - 'These settings only apply to the legacy Logs Stream app, and we do not recommend configuring them. Instead, use Logs Explorer which makes viewing and inspecting your logs easier with more features, better performance, and more intuitive navigation.', - } - ), + message: i18n.translate('xpack.infra.logsDeprecationCallout.settings.exploreWithDiscover', { + defaultMessage: + 'These settings only apply to the legacy Logs Stream app. Switch to Discover for the same functionality plus more features, better performance, and more intuitive navigation.', + }), }, }; @@ -60,10 +52,9 @@ export const LogsDeprecationCallout = ({ page }: LogsDeprecationCalloutProps) => const [isDismissed, setDismissed] = useLocalStorage(dismissalStorageKey, false); - const allDatasetLocator = - share.url.locators.get(ALL_DATASETS_LOCATOR_ID); + const discoverLocator = share.url.locators.get(DISCOVER_APP_LOCATOR); - if (isDismissed || !(allDatasetLocator && discover?.show && fleet?.read)) { + if (isDismissed || !(discoverLocator && discover?.show && fleet?.read)) { return null; } @@ -81,19 +72,19 @@ export const LogsDeprecationCallout = ({ page }: LogsDeprecationCalloutProps) =>

{message}

- {i18n.translate('xpack.infra.logsDeprecationCallout.tryLogsExplorerButtonLabel', { - defaultMessage: 'Try Logs Explorer', + {i18n.translate('xpack.infra.logsDeprecationCallout.goToDiscoverButtonLabel', { + defaultMessage: 'Go to Discover', })} ); }; -const getLogsExplorerLinkProps = (locator: LocatorPublic) => { +const getDiscoverLinkProps = (locator: LocatorPublic) => { return getRouterLinkProps({ href: locator.getRedirectUrl({}), onClick: () => locator.navigate({}), diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts index c66416331e4d0..19f7e47e84fce 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts @@ -74,7 +74,7 @@ describe('getDataStreamTypes', () => { it('should return metrics and entity source_data_stream types when entityCentriExperienceEnabled is true and has entity data', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(true); (getLatestEntity as jest.Mock).mockResolvedValue({ - 'source_data_stream.type': ['logs', 'metrics'], + sourceDataStreamType: ['logs', 'metrics'], }); const params = { @@ -118,7 +118,7 @@ describe('getDataStreamTypes', () => { it('should return entity source_data_stream types when has no metrics', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(false); (getLatestEntity as jest.Mock).mockResolvedValue({ - 'source_data_stream.type': ['logs', 'traces'], + sourceDataStreamType: ['logs', 'traces'], }); const params = { diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts index 3218ae257f1a2..f9b2d41bbe050 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts @@ -7,11 +7,9 @@ import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; -import { - EntityDataStreamType, - SOURCE_DATA_STREAM_TYPE, -} from '@kbn/observability-shared-plugin/common'; +import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common'; import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { castArray } from 'lodash'; import { type InfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client'; import { getHasMetricsData } from './get_has_metrics_data'; import { getLatestEntity } from './get_latest_entity'; @@ -45,15 +43,15 @@ export async function getDataStreamTypes({ return Array.from(sourceDataStreams); } - const entity = await getLatestEntity({ + const latestEntity = await getLatestEntity({ inventoryEsClient: obsEsClient, entityId, entityType, entityManagerClient, }); - if (entity?.[SOURCE_DATA_STREAM_TYPE]) { - [entity[SOURCE_DATA_STREAM_TYPE]].flat().forEach((item) => { + if (latestEntity) { + castArray(latestEntity.sourceDataStreamType).forEach((item) => { sourceDataStreams.add(item as EntityDataStreamType); }); } diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts index 7bcce2964fd13..31e778313f939 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts @@ -7,20 +7,16 @@ import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema'; import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; -import { - ENTITY_TYPE, - SOURCE_DATA_STREAM_TYPE, -} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; +import { ENTITY_TYPE, SOURCE_DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common'; import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; -import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ type: '*', dataset: ENTITY_LATEST, }); -interface Entity { - [SOURCE_DATA_STREAM_TYPE]: string | string[]; +interface EntitySourceResponse { + sourceDataStreamType?: string | string[]; } export async function getLatestEntity({ @@ -33,7 +29,7 @@ export async function getLatestEntity({ entityType: 'host' | 'container'; entityId: string; entityManagerClient: EntityClient; -}): Promise { +}): Promise { const { definitions } = await entityManagerClient.getEntityDefinitions({ builtIn: true, type: entityType, @@ -41,10 +37,12 @@ export async function getLatestEntity({ const hostOrContainerIdentityField = definitions[0]?.identityFields?.[0]?.field; if (hostOrContainerIdentityField === undefined) { - return { [SOURCE_DATA_STREAM_TYPE]: [] }; + return undefined; } - const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { + const response = await inventoryEsClient.esql<{ + source_data_stream?: { type?: string | string[] }; + }>('get_latest_entities', { query: `FROM ${ENTITIES_LATEST_ALIAS} | WHERE ${ENTITY_TYPE} == ? | WHERE ${hostOrContainerIdentityField} == ? @@ -53,5 +51,5 @@ export async function getLatestEntity({ params: [entityType, entityId], }); - return esqlResultToPlainObjects(latestEntitiesEsqlResponse)[0]; + return { sourceDataStreamType: response[0].source_data_stream?.type }; } diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx index d3d28fe040198..0188ed3143034 100644 --- a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx +++ b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx @@ -24,7 +24,14 @@ export function getMockInventoryContext(): InventoryKibanaContext { return { ...coreStart, - entityManager: {} as unknown as EntityManagerPublicPluginStart, + entityManager: { + entityClient: { + asKqlFilter: jest.fn(), + getIdentityFieldsValue() { + return 'entity_id'; + }, + }, + } as unknown as EntityManagerPublicPluginStart, observabilityShared: {} as unknown as ObservabilitySharedPluginStart, inference: {} as unknown as InferencePublicStart, share: { diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 3a9684a38254a..65fd8a4ffbd7a 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -4,24 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { z } from '@kbn/zod'; -import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema'; -import { - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; +import { ENTITY_LATEST, entitiesAliasPattern, type EntityMetadata } from '@kbn/entities-schema'; import { decode, encode } from '@kbn/rison'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; export const entityColumnIdsRt = t.union([ - t.literal(ENTITY_DISPLAY_NAME), - t.literal(ENTITY_LAST_SEEN), - t.literal(ENTITY_TYPE), + t.literal('entityDisplayName'), + t.literal('entityLastSeenTimestamp'), + t.literal('entityType'), t.literal('alertsCount'), t.literal('actions'), ]); @@ -80,23 +71,20 @@ export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ dataset: ENTITY_LATEST, }); -export interface Entity { - [ENTITY_LAST_SEEN]: string; - [ENTITY_ID]: string; - [ENTITY_TYPE]: string; - [ENTITY_DISPLAY_NAME]: string; - [ENTITY_DEFINITION_ID]: string; - [ENTITY_IDENTITY_FIELDS]: string | string[]; - alertsCount?: number; - [key: string]: any; -} - export type EntityGroup = { count: number; } & { [key: string]: string; }; -export type InventoryEntityLatest = z.infer & { +export type InventoryEntity = { + entityId: string; + entityType: string; + entityIdentityFields: string | string[]; + entityDisplayName: string; + entityDefinitionId: string; + entityLastSeenTimestamp: string; + entityDefinitionVersion: string; + entitySchemaVersion: string; alertsCount?: number; -}; +} & EntityMetadata; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts new file mode 100644 index 0000000000000..dccc888abd8dc --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AgentName } from '@kbn/elastic-agent-utils'; +import type { InventoryEntity } from '../entities'; + +interface BuiltinEntityMap { + host: InventoryEntity & { cloud?: { provider?: string[] } }; + container: InventoryEntity & { cloud?: { provider?: string[] } }; + service: InventoryEntity & { + agent?: { name: AgentName[] }; + service?: { environment?: string }; + }; +} + +export const isBuiltinEntityOfType = ( + type: T, + entity: InventoryEntity +): entity is BuiltinEntityMap[T] => { + return entity.entityType === type; +}; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts b/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts deleted file mode 100644 index 758d185a5753b..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts +++ /dev/null @@ -1,13 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { unflattenObject } from '@kbn/observability-utils/object/unflatten_object'; -import type { Entity, InventoryEntityLatest } from '../entities'; - -export function unflattenEntity(entity: Entity) { - return unflattenObject(entity) as InventoryEntityLatest; -} diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts index 9c9011609740b..17b6cf502280a 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts @@ -169,6 +169,7 @@ describe('Home page', () => { 'entityTypeControlGroupOptions' ); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); + cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); @@ -181,8 +182,6 @@ describe('Home page', () => { cy.get('server1').should('not.exist'); cy.contains('synth-node-trace-logs'); cy.contains('foo').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist'); }); it('Filters entities by host type', () => { @@ -193,6 +192,7 @@ describe('Home page', () => { 'entityTypeControlGroupOptions' ); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); + cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); @@ -205,8 +205,6 @@ describe('Home page', () => { cy.contains('server1'); cy.contains('synth-node-trace-logs').should('not.exist'); cy.contains('foo').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist'); }); it('Filters entities by container type', () => { @@ -217,6 +215,7 @@ describe('Home page', () => { 'entityTypeControlGroupOptions' ); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); + cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); @@ -229,8 +228,6 @@ describe('Home page', () => { cy.contains('server1').should('not.exist'); cy.contains('synth-node-trace-logs').should('not.exist'); cy.contains('foo'); - cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist'); }); it('Navigates to discover with actions button in the entities list', () => { 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 index b5244cb29f7fc..5195a35b93f4e 100644 --- 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 @@ -8,11 +8,16 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { AlertsBadge } from './alerts_badge'; import { useKibana } from '../../hooks/use_kibana'; -import type { Entity } from '../../../common/entities'; +import type { InventoryEntity } from '../../../common/entities'; jest.mock('../../hooks/use_kibana'); const useKibanaMock = useKibana as jest.Mock; +const commonEntityFields: Partial = { + entityLastSeenTimestamp: 'foo', + entityId: '1', +}; + describe('AlertsBadge', () => { const mockAsKqlFilter = jest.fn(); @@ -40,16 +45,19 @@ describe('AlertsBadge', () => { }); it('render alerts badge for a host entity', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'host', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'host.name', - 'host.name': 'foo', - 'entity.definition_id': 'host', - 'cloud.provider': null, + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityType: 'host', + entityDisplayName: 'foo', + entityIdentityFields: 'host.name', + entityDefinitionId: 'host', alertsCount: 1, + host: { + name: 'foo', + }, + cloud: { + provider: null, + }, }; mockAsKqlFilter.mockReturnValue('host.name: foo'); @@ -60,16 +68,22 @@ describe('AlertsBadge', () => { expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1'); }); it('render alerts badge for a service entity', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'agent.name': 'node', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'service.name', - 'service.name': 'bar', - 'entity.definition_id': 'host', - 'cloud.provider': null, + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityType: 'service', + entityDisplayName: 'foo', + entityIdentityFields: 'service.name', + entityDefinitionId: 'service', + service: { + name: 'bar', + }, + agent: { + name: 'node', + }, + cloud: { + provider: null, + }, + alertsCount: 5, }; mockAsKqlFilter.mockReturnValue('service.name: bar'); @@ -81,17 +95,22 @@ describe('AlertsBadge', () => { expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5'); }); it('render alerts badge for a service entity with multiple identity fields', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'agent.name': 'node', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': ['service.name', 'service.environment'], - 'service.name': 'bar', - 'service.environment': 'prod', - 'entity.definition_id': 'host', - 'cloud.provider': null, + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityType: 'service', + entityDisplayName: 'foo', + entityIdentityFields: ['service.name', 'service.environment'], + entityDefinitionId: 'service', + service: { + name: 'bar', + environment: 'prod', + }, + agent: { + name: 'node', + }, + cloud: { + provider: null, + }, alertsCount: 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 index a5845a7b42dcf..ed873bdb68c21 100644 --- 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 @@ -8,11 +8,10 @@ import React from 'react'; import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { Entity } from '../../../common/entities'; -import { unflattenEntity } from '../../../common/utils/unflatten_entity'; +import type { InventoryEntity } from '../../../common/entities'; import { useKibana } from '../../hooks/use_kibana'; -export function AlertsBadge({ entity }: { entity: Entity }) { +export function AlertsBadge({ entity }: { entity: InventoryEntity }) { const { services: { http: { basePath }, @@ -22,7 +21,12 @@ export function AlertsBadge({ entity }: { entity: Entity }) { const activeAlertsHref = basePath.prepend( `/app/observability/alerts?_a=${rison.encode({ - kuery: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)), + kuery: entityManager.entityClient.asKqlFilter({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + ...entity, + }), status: 'active', })}` ); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx index a3f2834934cd8..ae80bf09ecae2 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiDataGridSorting, EuiFlexGroup, EuiFlexItem } from '@elast import { Meta, Story } from '@storybook/react'; import { orderBy } from 'lodash'; import React, { useMemo, useState } from 'react'; -import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common'; import { useArgs } from '@storybook/addons'; import { EntitiesGrid } from '.'; import { entitiesMock } from './mock/entities_mock'; @@ -45,7 +45,7 @@ export const Grid: Story = (args) => { const filteredAndSortedItems = useMemo( () => orderBy( - entityType ? entitiesMock.filter((mock) => mock[ENTITY_TYPE] === entityType) : entitiesMock, + entityType ? entitiesMock.filter((mock) => mock.entityType === entityType) : entitiesMock, sort.id, sort.direction ), diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx index d5d08ed415a40..29a862646c4c4 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx @@ -9,28 +9,22 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { EntityName } from '.'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; -import { Entity } from '../../../../common/entities'; -import { - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; +import type { InventoryEntity } from '../../../../common/entities'; jest.mock('../../../hooks/use_detail_view_redirect'); const useDetailViewRedirectMock = useDetailViewRedirect as jest.Mock; describe('EntityName', () => { - const mockEntity: Entity = { - [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', - [ENTITY_ID]: '1', - [ENTITY_DISPLAY_NAME]: 'entity_name', - [ENTITY_DEFINITION_ID]: 'entity_definition_id', - [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], - [ENTITY_TYPE]: 'service', + const mockEntity: InventoryEntity = { + entityLastSeenTimestamp: '2023-10-09T00:00:00Z', + entityId: '1', + entityType: 'service', + entityDisplayName: 'entity_name', + entityIdentityFields: ['service.name', 'service.environment'], + entityDefinitionId: 'entity_definition_id', + entitySchemaVersion: '1', + entityDefinitionVersion: '1', }; beforeEach(() => { diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index e8db7013f8cb3..6117f6e428bde 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -7,14 +7,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { ENTITY_DISPLAY_NAME } from '@kbn/observability-shared-plugin/common'; import { useKibana } from '../../../hooks/use_kibana'; -import type { Entity } from '../../../../common/entities'; +import type { InventoryEntity } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; interface EntityNameProps { - entity: Entity; + entity: InventoryEntity; } export function EntityName({ entity }: EntityNameProps) { @@ -29,7 +28,7 @@ export function EntityName({ entity }: EntityNameProps) { const handleLinkClick = useCallback(() => { telemetry.reportEntityViewClicked({ view_type: 'detail', - entity_type: entity['entity.type'], + entity_type: entity.entityType, }); }, [entity, telemetry]); @@ -40,7 +39,7 @@ export function EntityName({ entity }: EntityNameProps) { - {entity[ENTITY_DISPLAY_NAME]} + {entity.entityDisplayName} 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 index d514dc9199aec..be5c50eba9c07 100644 --- 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 @@ -8,11 +8,6 @@ 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', @@ -76,12 +71,12 @@ export const getColumns = ({ }: { showAlertsColumn: boolean; showActions: boolean; -}): EuiDataGridColumn[] => { +}) => { return [ ...(showAlertsColumn ? [ { - id: 'alertsCount', + id: 'alertsCount' as const, displayAsText: alertsLabel, isSortable: true, display: , @@ -91,21 +86,21 @@ export const getColumns = ({ ] : []), { - id: ENTITY_DISPLAY_NAME, + id: 'entityDisplayName' as const, // keep it for accessibility purposes displayAsText: entityNameLabel, display: , isSortable: true, }, { - id: ENTITY_TYPE, + id: 'entityType' as const, // keep it for accessibility purposes displayAsText: entityTypeLabel, display: , isSortable: true, }, { - id: ENTITY_LAST_SEEN, + id: 'entityLastSeenTimestamp' as const, // keep it for accessibility purposes displayAsText: entityLastSeenLabel, display: ( @@ -118,7 +113,7 @@ export const getColumns = ({ ...(showActions ? [ { - id: 'actions', + id: 'actions' as const, // keep it for accessibility purposes displayAsText: entityActionsLabel, display: ( @@ -128,5 +123,5 @@ export const getColumns = ({ }, ] : []), - ]; + ] satisfies EuiDataGridColumn[]; }; 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 7ca29f7820332..ff4329955773d 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 @@ -15,13 +15,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; import { last } from 'lodash'; import React, { useCallback, useMemo } from 'react'; -import { - ENTITY_DISPLAY_NAME, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; -import { EntityColumnIds } from '../../../common/entities'; -import { APIReturnType } from '../../api'; +import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { EntityColumnIds, InventoryEntity } from '../../../common/entities'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; import { getColumns } from './grid_columns'; import { AlertsBadge } from '../alerts_badge/alerts_badge'; @@ -29,12 +24,9 @@ import { EntityName } from './entity_name'; import { EntityActions } from '../entity_actions'; import { useDiscoverRedirect } from '../../hooks/use_discover_redirect'; -type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; -type LatestEntities = InventoryEntitiesAPIReturnType['entities']; - interface Props { loading: boolean; - entities: LatestEntities; + entities: InventoryEntity[]; sortDirection: 'asc' | 'desc'; sortField: string; pageIndex: number; @@ -88,16 +80,17 @@ export function EntitiesGrid({ } const columnEntityTableId = columnId as EntityColumnIds; - const entityType = entity[ENTITY_TYPE]; + const entityType = entity.entityType; const discoverUrl = getDiscoverRedirectUrl(entity); switch (columnEntityTableId) { case 'alertsCount': return entity?.alertsCount ? : null; - case ENTITY_TYPE: + case 'entityType': return ; - case ENTITY_LAST_SEEN: + + case 'entityLastSeenTimestamp': return ( ); - case ENTITY_DISPLAY_NAME: + case 'entityDisplayName': return ; case 'actions': return ( discoverUrl && ( ) ); default: - return entity[columnId as EntityColumnIds] || ''; + return null; } }, [entities, getDiscoverRedirectUrl] 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 3b7e7afcadb99..1048b18f82e91 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 @@ -6,15 +6,7 @@ */ import { faker } from '@faker-js/faker'; -import { - ENTITY_DISPLAY_NAME, - ENTITY_TYPE, - ENTITY_ID, - ENTITY_LAST_SEEN, - AGENT_NAME, - CLOUD_PROVIDER, -} from '@kbn/observability-shared-plugin/common'; -import { Entity } from '../../../../common/entities'; +import type { InventoryEntity } from '../../../../common/entities'; const idGenerator = () => { let id = 0; @@ -33,38 +25,48 @@ function generateRandomTimestamp() { return randomDate.toISOString(); } -const getEntity = (entityType: string, customFields: Record = {}) => ({ - [ENTITY_LAST_SEEN]: generateRandomTimestamp(), - [ENTITY_TYPE]: entityType, - [ENTITY_DISPLAY_NAME]: faker.person.fullName(), - [ENTITY_ID]: generateId(), - ...customFields, +const indentityFieldsPerType: Record = { + host: ['host.name'], + container: ['container.id'], + service: ['service.name'], +}; + +const getEntityLatest = ( + entityType: string, + overrides?: Partial +): InventoryEntity => ({ + entityLastSeenTimestamp: generateRandomTimestamp(), + entityType, + entityDisplayName: faker.person.fullName(), + entityId: generateId(), + entityDefinitionId: faker.string.uuid(), + entityDefinitionVersion: '1.0.0', + entityIdentityFields: indentityFieldsPerType[entityType], + entitySchemaVersion: '1.0.0', + ...overrides, }); -const alertsMock = [ - { - ...getEntity('host'), - alertsCount: 3, - }, - { - ...getEntity('service'), +const alertsMock: InventoryEntity[] = [ + getEntityLatest('host', { + alertsCount: 1, + }), + getEntityLatest('service', { alertsCount: 3, - }, - - { - ...getEntity('host'), + }), + getEntityLatest('host', { alertsCount: 10, - }, - { - ...getEntity('host'), + }), + getEntityLatest('host', { alertsCount: 1, - }, + }), ]; -const hostsMock = Array.from({ length: 20 }, () => getEntity('host', { [CLOUD_PROVIDER]: 'gcp' })); -const containersMock = Array.from({ length: 20 }, () => getEntity('container')); +const hostsMock = Array.from({ length: 20 }, () => + getEntityLatest('host', { cloud: { provider: 'gcp' } }) +); +const containersMock = Array.from({ length: 20 }, () => getEntityLatest('container')); const servicesMock = Array.from({ length: 20 }, () => - getEntity('service', { [AGENT_NAME]: 'java' }) + getEntityLatest('service', { agent: { name: 'java' } }) ); export const entitiesMock = [ @@ -72,4 +74,4 @@ export const entitiesMock = [ ...hostsMock, ...containersMock, ...servicesMock, -] as Entity[]; +] as InventoryEntity[]; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 48b21779d2e38..4da8fd3103c41 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -6,36 +6,23 @@ */ import React from 'react'; -import { - AGENT_NAME, - CLOUD_PROVIDER, - ENTITY_TYPE, - ENTITY_TYPES, -} from '@kbn/observability-shared-plugin/common'; import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import type { AgentName } from '@kbn/elastic-agent-utils'; import { euiThemeVars } from '@kbn/ui-theme'; -import type { Entity } from '../../../common/entities'; +import { castArray } from 'lodash'; +import type { InventoryEntity } from '../../../common/entities'; +import { isBuiltinEntityOfType } from '../../../common/utils/entity_type_guards'; interface EntityIconProps { - entity: Entity; + entity: InventoryEntity; } -type NotNullableCloudProvider = Exclude; - -const getSingleValue = (value?: T | T[] | null): T | undefined => { - return value == null ? undefined : Array.isArray(value) ? value[0] : value; -}; - export function EntityIcon({ entity }: EntityIconProps) { - const entityType = entity[ENTITY_TYPE]; const defaultIconSize = euiThemeVars.euiSizeL; - if (entityType === ENTITY_TYPES.HOST || entityType === ENTITY_TYPES.CONTAINER) { - const cloudProvider = getSingleValue( - entity[CLOUD_PROVIDER] as NotNullableCloudProvider | NotNullableCloudProvider[] - ); + if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) { + const cloudProvider = castArray(entity.cloud?.provider)[0]; + return ( ; + if (isBuiltinEntityOfType('service', entity)) { + return ; } - if (entityType.startsWith('kubernetes')) { + if (entity.entityType.startsWith('kubernetes')) { return ; } diff --git a/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx index b939f0fa5c423..0964b7bb39465 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx @@ -8,6 +8,7 @@ import { EuiSpacer } from '@elastic/eui'; import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import React from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { flattenObject } from '@kbn/observability-utils/object/flatten_object'; import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async'; import { useKibana } from '../../hooks/use_kibana'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context'; @@ -52,15 +53,18 @@ export function GroupedInventory() { <> - {value.groups.map((group) => ( - - ))} + {value.groups.map((group) => { + const groupValue = flattenObject(group)[value.groupBy]; + return ( + + ); + })} ); } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index cf4993f871880..233c1a1076b79 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -9,34 +9,24 @@ import { renderHook } from '@testing-library/react-hooks'; import { useDetailViewRedirect } from './use_detail_view_redirect'; import { useKibana } from './use_kibana'; import { - AGENT_NAME, - CLOUD_PROVIDER, CONTAINER_ID, - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, - ENTITY_TYPE, - HOST_NAME, ENTITY_TYPES, - SERVICE_ENVIRONMENT, + HOST_NAME, SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; -import { unflattenEntity } from '../../common/utils/unflatten_entity'; -import type { Entity } from '../../common/entities'; +import type { InventoryEntity } from '../../common/entities'; jest.mock('./use_kibana'); -jest.mock('../../common/utils/unflatten_entity'); const useKibanaMock = useKibana as jest.Mock; -const unflattenEntityMock = unflattenEntity as jest.Mock; -const commonEntityFields: Partial = { - [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', - [ENTITY_ID]: '1', - [ENTITY_DISPLAY_NAME]: 'entity_name', - [ENTITY_DEFINITION_ID]: 'entity_definition_id', +const commonEntityFields: Partial = { + entityLastSeenTimestamp: '2023-10-09T00:00:00Z', + entityId: '1', + entityDisplayName: 'entity_name', + entityDefinitionId: 'entity_definition_id', + entityDefinitionVersion: '1', + entitySchemaVersion: '1', }; describe('useDetailViewRedirect', () => { @@ -66,17 +56,19 @@ describe('useDetailViewRedirect', () => { }, }, }); - - unflattenEntityMock.mockImplementation((entity) => entity); }); it('getEntityRedirectUrl should return the correct URL for host entity', () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: [HOST_NAME], - [ENTITY_TYPE]: 'host', - [HOST_NAME]: 'host-1', - [CLOUD_PROVIDER]: null, + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityType: 'host', + entityIdentityFields: ['host.name'], + host: { + name: 'host-1', + }, + cloud: { + provider: null, + }, }; mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' }); @@ -90,12 +82,16 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for container entity', () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: [CONTAINER_ID], - [ENTITY_TYPE]: 'container', - [CONTAINER_ID]: 'container-1', - [CLOUD_PROVIDER]: null, + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityType: 'container', + entityIdentityFields: ['container.id'], + container: { + id: 'container-1', + }, + cloud: { + provider: null, + }, }; mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' }); @@ -112,13 +108,17 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for service entity', () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: [SERVICE_NAME], - [ENTITY_TYPE]: 'service', - [SERVICE_NAME]: 'service-1', - [SERVICE_ENVIRONMENT]: 'prod', - [AGENT_NAME]: 'node', + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityType: 'service', + entityIdentityFields: ['service.name'], + agent: { + name: 'node', + }, + service: { + name: 'service-1', + environment: 'prod', + }, }; mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' }); mockGetRedirectUrl.mockReturnValue('service-overview-url'); @@ -145,10 +145,13 @@ describe('useDetailViewRedirect', () => { [ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'], ].forEach(([entityType, dashboardId]) => { it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: ['some.field'], - [ENTITY_TYPE]: entityType, + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityType, + entityIdentityFields: ['some.field'], + some: { + field: 'some-value', + }, }; mockAsKqlFilter.mockReturnValue('kql-query'); diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 23380dc3704de..4df4fa4ca1f96 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -6,20 +6,17 @@ */ import { ASSET_DETAILS_LOCATOR_ID, - AssetDetailsLocatorParams, - ENTITY_IDENTITY_FIELDS, - ENTITY_TYPE, ENTITY_TYPES, - SERVICE_ENVIRONMENT, SERVICE_OVERVIEW_LOCATOR_ID, - ServiceOverviewParams, + type AssetDetailsLocatorParams, + type ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; import { useCallback } from 'react'; -import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; +import type { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; -import type { Entity } from '../../common/entities'; -import { unflattenEntity } from '../../common/utils/unflatten_entity'; +import { isBuiltinEntityOfType } from '../../common/utils/entity_type_guards'; +import type { InventoryEntity } from '../../common/entities'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { @@ -44,52 +41,38 @@ export const useDetailViewRedirect = () => { const dashboardLocator = locators.get(DASHBOARD_APP_LOCATOR); const serviceOverviewLocator = locators.get(SERVICE_OVERVIEW_LOCATOR_ID); - const getSingleIdentityFieldValue = useCallback( - (entity: Entity) => { - const identityFields = castArray(entity[ENTITY_IDENTITY_FIELDS]); - if (identityFields.length > 1) { - throw new Error(`Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}`); - } - - const identityField = identityFields[0]; - return entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity))[ - identityField - ]; - }, - [entityManager.entityClient] - ); - const getDetailViewRedirectUrl = useCallback( - (entity: Entity) => { - const type = entity[ENTITY_TYPE]; - const identityValue = getSingleIdentityFieldValue(entity); - - switch (type) { - case ENTITY_TYPES.HOST: - case ENTITY_TYPES.CONTAINER: - return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue, - assetType: type, - }); + (entity: InventoryEntity) => { + const identityFieldsValue = entityManager.entityClient.getIdentityFieldsValue({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + ...entity, + }); + const identityFields = castArray(entity.entityIdentityFields); - case 'service': - return serviceOverviewLocator?.getRedirectUrl({ - serviceName: identityValue, - // service.environemnt is not part of entity.identityFields - // we need to manually get its value - environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0], - }); + if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) { + return assetDetailsLocator?.getRedirectUrl({ + assetId: identityFieldsValue[identityFields[0]], + assetType: entity.entityType, + }); + } - default: - return undefined; + if (isBuiltinEntityOfType('service', entity)) { + return serviceOverviewLocator?.getRedirectUrl({ + serviceName: identityFieldsValue[identityFields[0]], + environment: entity.service?.environment, + }); } + + return undefined; }, - [assetDetailsLocator, getSingleIdentityFieldValue, serviceOverviewLocator] + [assetDetailsLocator, entityManager.entityClient, serviceOverviewLocator] ); const getDashboardRedirectUrl = useCallback( - (entity: Entity) => { - const type = entity[ENTITY_TYPE]; + (entity: InventoryEntity) => { + const type = entity.entityType; const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; return dashboardId @@ -97,7 +80,12 @@ export const useDetailViewRedirect = () => { dashboardId, query: { language: 'kuery', - query: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)), + query: entityManager.entityClient.asKqlFilter({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + ...entity, + }), }, }) : undefined; @@ -106,7 +94,8 @@ export const useDetailViewRedirect = () => { ); const getEntityRedirectUrl = useCallback( - (entity: Entity) => getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity), + (entity: InventoryEntity) => + getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity), [getDashboardRedirectUrl, getDetailViewRedirectUrl] ); diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts index c29caca7e5b77..33758c9df449d 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts @@ -11,12 +11,11 @@ import { ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; import { useCallback } from 'react'; -import type { Entity, EntityColumnIds } from '../../common/entities'; -import { unflattenEntity } from '../../common/utils/unflatten_entity'; +import type { InventoryEntity } from '../../common/entities'; import { useKibana } from './use_kibana'; import { useUnifiedSearchContext } from './use_unified_search_context'; -const ACTIVE_COLUMNS: EntityColumnIds[] = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN]; +const ACTIVE_COLUMNS = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN]; export const useDiscoverRedirect = () => { const { @@ -31,9 +30,14 @@ export const useDiscoverRedirect = () => { const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR'); const getDiscoverEntitiesRedirectUrl = useCallback( - (entity?: Entity) => { + (entity?: InventoryEntity) => { const entityKqlFilter = entity - ? entityManager.entityClient.asKqlFilter(unflattenEntity(entity)) + ? entityManager.entityClient.asKqlFilter({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + ...entity, + }) : ''; const kueryWithEntityDefinitionFilters = [ @@ -65,7 +69,7 @@ export const useDiscoverRedirect = () => { ); const getDiscoverRedirectUrl = useCallback( - (entity?: Entity) => getDiscoverEntitiesRedirectUrl(entity), + (entity?: InventoryEntity) => getDiscoverEntitiesRedirectUrl(entity), [getDiscoverEntitiesRedirectUrl] ); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts index 8c72e18bc0740..bab4af50e316e 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts @@ -7,7 +7,6 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; -import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; import { ENTITIES_LATEST_ALIAS, type EntityGroup, @@ -32,10 +31,8 @@ export async function getEntityGroupsBy({ const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`; const query = [from, ...where, group, sort, limit].join(' | '); - const groups = await inventoryEsClient.esql('get_entities_groups', { + return inventoryEsClient.esql('get_entities_groups', { query, filter: esQuery, }); - - return esqlResultToPlainObjects(groups); } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index 2dfc9b8ccfdf3..99b8829b600b2 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -7,6 +7,7 @@ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import type { EntityInstance } from '@kbn/entities-schema'; import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; @@ -15,12 +16,14 @@ export async function getEntityTypes({ }: { inventoryEsClient: ObservabilityElasticsearchClient; }) { - const entityTypesEsqlResponse = await inventoryEsClient.esql('get_entity_types', { + const entityTypesEsqlResponse = await inventoryEsClient.esql<{ + entity: Pick; + }>('get_entity_types', { query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE} `, }); - return entityTypesEsqlResponse.values.map(([_, val]) => val as string); + return entityTypesEsqlResponse.map((response) => response.entity.type); } 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 index 62d77c08fd27a..8b6b3b109352c 100644 --- 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 @@ -5,52 +5,60 @@ * 2.0. */ -import type { Entity } from '../../../common/entities'; -import { - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, -} from '@kbn/observability-shared-plugin/common'; +import type { InventoryEntity } from '../../../common/entities'; 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, +const commonEntityFields: Partial = { + entityLastSeenTimestamp: '2023-10-09T00:00:00Z', + entityId: '1', + entityDisplayName: 'entity_name', + entityDefinitionId: 'entity_definition_id', + alertsCount: 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: Entity = { - 'agent.name': 'node', - [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], - 'service.name': 'my-service', - 'entity.type': 'service', - ...commonEntityFields, + const serviceEntity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityIdentityFields: ['service.name', 'service.environment'], + entityType: 'service', + agent: { + name: 'node', + }, + service: { + name: 'my-service', + }, }; - const hostEntity: Entity = { - [ENTITY_IDENTITY_FIELDS]: ['host.name'], - 'host.name': 'my-host', - 'entity.type': 'host', - 'cloud.provider': null, - ...commonEntityFields, + const hostEntity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityIdentityFields: ['host.name'], + entityType: 'host', + cloud: { + provider: null, + }, + host: { + name: 'my-host', + }, }; - const containerEntity: Entity = { - [ENTITY_IDENTITY_FIELDS]: 'container.id', - 'host.name': 'my-host', - 'entity.type': 'container', - 'cloud.provider': null, - 'container.id': '123', - ...commonEntityFields, + const containerEntity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), + entityIdentityFields: ['container.id'], + entityType: 'container', + host: { + name: 'my-host', + }, + cloud: { + provider: null, + }, + container: { + id: '123', + }, }; const mockEntities = [serviceEntity, hostEntity, containerEntity]; 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 index f54dc8a7f121f..06070b66bad1f 100644 --- 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 @@ -5,16 +5,16 @@ * 2.0. */ -import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { Entity } from '../../../common/entities'; +import { castArray } from 'lodash'; +import type { InventoryEntity } from '../../../common/entities'; export type IdentityFieldsPerEntityType = Map; -export const getIdentityFieldsPerEntityType = (entities: Entity[]) => { - const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map(); +export const getIdentityFieldsPerEntityType = (latestEntities: InventoryEntity[]) => { + const identityFieldsPerEntityType = new Map(); - entities.forEach((entity) => - identityFieldsPerEntityType.set(entity[ENTITY_TYPE], [entity[ENTITY_IDENTITY_FIELDS]].flat()) + latestEntities.forEach((entity) => + identityFieldsPerEntityType.set(entity.entityType, castArray(entity.entityIdentityFields)) ); 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 402d11720a9da..7a65ac5039615 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 @@ -5,18 +5,32 @@ * 2.0. */ +import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { + ENTITY_LAST_SEEN, + ENTITY_TYPE, + ENTITY_DISPLAY_NAME, +} from '@kbn/observability-shared-plugin/common'; import type { QueryDslQueryContainer, ScalarValue } from '@elastic/elasticsearch/lib/api/types'; -import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; -import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import type { EntityInstance } from '@kbn/entities-schema'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, - type Entity, type EntityColumnIds, + InventoryEntity, } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; +type EntitySortableColumnIds = Extract< + EntityColumnIds, + 'entityLastSeenTimestamp' | 'entityDisplayName' | 'entityType' +>; +const SORT_FIELDS_TO_ES_FIELDS: Record = { + entityLastSeenTimestamp: ENTITY_LAST_SEEN, + entityDisplayName: ENTITY_DISPLAY_NAME, + entityType: ENTITY_TYPE, +} as const; + export async function getLatestEntities({ inventoryEsClient, sortDirection, @@ -29,9 +43,10 @@ export async function getLatestEntities({ sortField: EntityColumnIds; esQuery?: QueryDslQueryContainer; entityTypes?: string[]; -}) { +}): Promise { // 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 entitiesSortField = + SORT_FIELDS_TO_ES_FIELDS[sortField as EntitySortableColumnIds] ?? ENTITY_LAST_SEEN; const from = `FROM ${ENTITIES_LATEST_ALIAS}`; const where: string[] = [getBuiltinEntityDefinitionIdESQLWhereClause()]; @@ -47,11 +62,28 @@ export async function getLatestEntities({ const query = [from, ...where, sort, limit].join(' | '); - const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { - query, - filter: esQuery, - params, - }); + const latestEntitiesEsqlResponse = await inventoryEsClient.esql( + 'get_latest_entities', + { + query, + filter: esQuery, + params, + } + ); - return esqlResultToPlainObjects(latestEntitiesEsqlResponse); + return latestEntitiesEsqlResponse.map((lastestEntity) => { + const { entity, ...metadata } = lastestEntity; + + return { + entityId: entity.id, + entityType: entity.type, + entityDefinitionId: entity.definition_id, + entityDisplayName: entity.display_name, + entityIdentityFields: entity.identity_fields, + entityLastSeenTimestamp: entity.last_seen_timestamp, + entityDefinitionVersion: entity.definition_version, + entitySchemaVersion: entity.schema_version, + ...metadata, + }; + }); } 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 index 8126c69de6922..0f1ace0407233 100644 --- 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 @@ -6,7 +6,6 @@ */ import { termQuery } from '@kbn/observability-plugin/server'; -import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; 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'; @@ -25,7 +24,7 @@ export async function getLatestEntitiesAlerts({ }: { alertsClient: AlertsClient; identityFieldsPerEntityType: IdentityFieldsPerEntityType; -}): Promise> { +}): Promise> { if (identityFieldsPerEntityType.size === 0) { return []; } @@ -54,7 +53,7 @@ export async function getLatestEntitiesAlerts({ return buckets.map((bucket: Bucket) => ({ alertsCount: bucket.doc_count, - [ENTITY_TYPE]: entityType, + entityType, ...bucket.key, })); }); 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 ae99713375b19..bdf0b1f59af01 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 @@ -11,7 +11,7 @@ import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import * as t from 'io-ts'; import { orderBy } from 'lodash'; import { joinByKey } from '@kbn/observability-utils/array/join_by_key'; -import { entityColumnIdsRt, Entity } from '../../../common/entities'; +import { entityColumnIdsRt, InventoryEntity } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; @@ -61,7 +61,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ logger, plugins, request, - }): Promise<{ entities: Entity[] }> => { + }): Promise<{ entities: InventoryEntity[] }> => { const coreContext = await context.core; const inventoryEsClient = createObservabilityEsClient({ client: coreContext.elasticsearch.client.asCurrentUser, @@ -90,16 +90,16 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ }); const joined = joinByKey( - [...latestEntities, ...alerts], + [...latestEntities, ...alerts] as InventoryEntity[], [...identityFieldsPerEntityType.values()].flat() - ).filter((entity) => entity['entity.id']) as Entity[]; + ).filter((latestEntity) => latestEntity.entityId); return { entities: sortField === 'alertsCount' ? orderBy( joined, - [(item: Entity) => item?.alertsCount === undefined, sortField], + [(item: InventoryEntity) => item?.alertsCount === undefined, sortField], ['asc', sortDirection] // push entities without alertsCount to the end ) : joined, diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts index c1e4a82c343b0..d328a4f3b8d6f 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -5,7 +5,6 @@ * 2.0. */ import type { Logger } from '@kbn/core/server'; -import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from '../entities/query_helper'; import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; @@ -18,14 +17,15 @@ export async function getHasData({ logger: Logger; }) { try { - const esqlResults = await inventoryEsClient.esql('get_has_data', { + const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', { query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS _count = COUNT(*) | LIMIT 1`, }); - const totalCount = esqlResultToPlainObjects(esqlResults)?.[0]._count ?? 0; + const totalCount = esqlResults[0]._count; + return { hasData: totalCount > 0 }; } catch (e) { logger.error(e); diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 5cb95e8ac6de5..e9949e60201c8 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -53,7 +53,6 @@ "@kbn/spaces-plugin", "@kbn/cloud-plugin", "@kbn/storybook", - "@kbn/zod", "@kbn/dashboard-plugin", "@kbn/deeplinks-analytics", "@kbn/controls-plugin", diff --git a/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc index 9b8284808a657..e105cacf75d05 100644 --- a/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc +++ b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc @@ -23,7 +23,8 @@ "security", "observability", "licensing", - "ruleRegistry" + "ruleRegistry", + "usageCollection" ], "requiredBundles": [ "esql", diff --git a/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/fetcher.test.ts b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/fetcher.test.ts new file mode 100644 index 0000000000000..e13ae951975bf --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/fetcher.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClientMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; +import { CollectorFetchContext } from '@kbn/usage-collection-plugin/server'; +import { fetcher } from './fetcher'; + +let savedObjectClient: ReturnType; + +let closeMock: jest.Mock; +let esClient: ElasticsearchClientMock; + +describe('Investigation usage collector fetcher', () => { + beforeEach(() => { + savedObjectClient = savedObjectsRepositoryMock.create(); + closeMock = jest.fn(); + }); + + it('without any existing investigation', async () => { + savedObjectClient.createPointInTimeFinder.mockReturnValue({ + find: async function* find() { + return { + [Symbol.asyncIterator]: async () => {}, + next: () => {}, + }; + }, + close: closeMock, + }); + + const results = await fetcher({ + soClient: savedObjectClient, + esClient, + } as CollectorFetchContext); + + expect(closeMock).toHaveBeenCalled(); + expect(results.investigation).toMatchInlineSnapshot(` + Object { + "by_origin": Object { + "alert": 0, + "blank": 0, + }, + "by_status": Object { + "active": 0, + "cancelled": 0, + "mitigated": 0, + "resolved": 0, + "triage": 0, + }, + "items": Object { + "avg": 0, + "max": 0, + "min": 0, + "p90": 0, + "p95": 0, + }, + "notes": Object { + "avg": 0, + "max": 0, + "min": 0, + "p90": 0, + "p95": 0, + }, + "total": 0, + } + `); + }); +}); diff --git a/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/fetcher.ts b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/fetcher.ts new file mode 100644 index 0000000000000..9f21e39e999a0 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/fetcher.ts @@ -0,0 +1,84 @@ +/* + * 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 { CollectorFetchContext } from '@kbn/usage-collection-plugin/server'; +import { StoredInvestigation } from '../../models/investigation'; +import { SO_INVESTIGATION_TYPE } from '../../saved_objects/investigation'; +import { computeMetrics } from './helpers/metrics'; +import { Usage } from './type'; + +export const fetcher = async (context: CollectorFetchContext) => { + const finder = context.soClient.createPointInTimeFinder({ + type: SO_INVESTIGATION_TYPE, + perPage: 10, + }); + + let usage: Usage['investigation'] = { + total: 0, + by_status: { + triage: 0, + active: 0, + mitigated: 0, + resolved: 0, + cancelled: 0, + }, + by_origin: { + alert: 0, + blank: 0, + }, + items: { + avg: 0, + p90: 0, + p95: 0, + max: 0, + min: 0, + }, + notes: { + avg: 0, + p90: 0, + p95: 0, + max: 0, + min: 0, + }, + }; + + const items: number[] = []; + const notes: number[] = []; + + for await (const response of finder.find()) { + usage = response.saved_objects.reduce((acc, so) => { + items.push(so.attributes.items.length); + notes.push(so.attributes.notes.length); + + return { + ...acc, + total: acc.total + 1, + by_status: { + ...acc.by_status, + ...(so.attributes.status === 'triage' && { triage: acc.by_status.triage + 1 }), + ...(so.attributes.status === 'active' && { active: acc.by_status.active + 1 }), + ...(so.attributes.status === 'mitigated' && { mitigated: acc.by_status.mitigated + 1 }), + ...(so.attributes.status === 'resolved' && { resolved: acc.by_status.resolved + 1 }), + ...(so.attributes.status === 'cancelled' && { cancelled: acc.by_status.cancelled + 1 }), + }, + by_origin: { + ...acc.by_origin, + ...(so.attributes.origin.type === 'alert' && { alert: acc.by_origin.alert + 1 }), + ...(so.attributes.origin.type === 'blank' && { blank: acc.by_origin.blank + 1 }), + }, + }; + }, usage); + } + + usage.items = computeMetrics(items.sort()); + usage.notes = computeMetrics(notes.sort()); + + await finder.close(); + + return { + investigation: usage, + }; +}; diff --git a/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/helpers/metrics.test.ts b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/helpers/metrics.test.ts new file mode 100644 index 0000000000000..d4e8964b95751 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/helpers/metrics.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { computeMetrics } from './metrics'; + +describe('ComputeMetrics', () => { + it('computes the metrics correctly', async () => { + expect(computeMetrics([])).toMatchInlineSnapshot(` + Object { + "avg": 0, + "max": 0, + "min": 0, + "p90": 0, + "p95": 0, + } + `); + expect(computeMetrics([10, 10, 100])).toMatchInlineSnapshot(` + Object { + "avg": 40, + "max": 100, + "min": 10, + "p90": 100, + "p95": 100, + } + `); + + const arr = Array.from({ length: 100 }, (_, i) => i); + expect(computeMetrics(arr)).toMatchInlineSnapshot(` + Object { + "avg": 49.5, + "max": 99, + "min": 0, + "p90": 90, + "p95": 95, + } + `); + }); +}); diff --git a/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/helpers/metrics.ts b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/helpers/metrics.ts new file mode 100644 index 0000000000000..a6a4a0b28760d --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/helpers/metrics.ts @@ -0,0 +1,32 @@ +/* + * 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 { sum } from 'lodash'; + +export function computeMetrics(arr: number[]) { + if (arr.length === 0) { + return { + avg: 0, + p90: 0, + p95: 0, + max: 0, + min: 0, + }; + } + + const total = sum(arr); + const r90 = (90 / 100) * (arr.length - 1) + 1; + const r95 = (95 / 100) * (arr.length - 1) + 1; + + return { + avg: total / arr.length, + p90: arr[Math.floor(r90)], + p95: arr[Math.floor(r95)], + max: arr[arr.length - 1], + min: arr[0], + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/register.ts b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/register.ts new file mode 100644 index 0000000000000..56c88eb322807 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/register.ts @@ -0,0 +1,147 @@ +/* + * 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 { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; +import { fetcher } from './fetcher'; +import type { Usage } from './type'; + +export function registerUsageCollector(usageCollection?: UsageCollectionSetup): void { + if (!usageCollection) { + return; + } + + const usageCollector = usageCollection.makeUsageCollector({ + type: 'investigation', + schema: { + investigation: { + total: { + type: 'long', + _meta: { + description: 'The total number of investigations in the cluster', + }, + }, + by_status: { + triage: { + type: 'long', + _meta: { + description: 'The number of investigations in triage status in the cluster', + }, + }, + active: { + type: 'long', + _meta: { description: 'The number of investigations in active status in the cluster' }, + }, + mitigated: { + type: 'long', + _meta: { + description: 'The number of investigations in mitigated status in the cluster', + }, + }, + resolved: { + type: 'long', + _meta: { + description: 'The number of investigations in resolved status in the cluster', + }, + }, + cancelled: { + type: 'long', + _meta: { + description: 'The number of investigations in cancelled status in the cluster', + }, + }, + }, + by_origin: { + alert: { + type: 'long', + _meta: { + description: 'The number of investigations created from alerts in the cluster', + }, + }, + blank: { + type: 'long', + _meta: { + description: 'The number of investigations created from scratch in the cluster', + }, + }, + }, + items: { + avg: { + type: 'long', + _meta: { + description: 'The average number of items across all investigations in the cluster', + }, + }, + p90: { + type: 'long', + _meta: { + description: + 'The 90th percentile of the number of items across all investigations in the cluster', + }, + }, + p95: { + type: 'long', + _meta: { + description: + 'The 95th percentile of the number of items across all investigations in the cluster', + }, + }, + max: { + type: 'long', + _meta: { + description: 'The maximum number of items across all investigations in the cluster', + }, + }, + min: { + type: 'long', + _meta: { + description: 'The minimum number of items across all investigations in the cluster', + }, + }, + }, + notes: { + avg: { + type: 'long', + _meta: { + description: 'The average number of notes across all investigations in the cluster', + }, + }, + p90: { + type: 'long', + _meta: { + description: + 'The 90th percentile of the number of notes across all investigations in the cluster', + }, + }, + p95: { + type: 'long', + _meta: { + description: + 'The 95th percentile of the number of notes across all investigations in the cluster', + }, + }, + max: { + type: 'long', + _meta: { + description: 'The maximum number of notes across all investigations in the cluster', + }, + }, + min: { + type: 'long', + _meta: { + description: 'The minimum number of notes across all investigations in the cluster', + }, + }, + }, + }, + }, + isReady: () => true, + fetch: fetcher, + }); + + // register usage collector + usageCollection.registerCollector(usageCollector); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/type.ts b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/type.ts new file mode 100644 index 0000000000000..b7d4215195b4d --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/lib/collectors/type.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Usage { + investigation: { + total: number; + by_status: { + triage: number; + active: number; + mitigated: number; + resolved: number; + cancelled: number; + }; + by_origin: { + alert: number; + blank: number; + }; + items: { + avg: number; + p90: number; + p95: number; + max: number; + min: number; + }; + notes: { + avg: number; + p90: number; + p95: number; + max: number; + min: number; + }; + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/plugin.ts b/x-pack/plugins/observability_solution/investigate_app/server/plugin.ts index f1ee1cacd155b..ec710cffa3b8d 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/plugin.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/plugin.ts @@ -19,6 +19,7 @@ import type { } from './types'; import { investigation } from './saved_objects/investigation'; import { InvestigateAppConfig } from './config'; +import { registerUsageCollector } from './lib/collectors/register'; export class InvestigateAppPlugin implements @@ -53,6 +54,7 @@ export class InvestigateAppPlugin if (this.config.enabled === true) { coreSetup.savedObjects.registerType(investigation); + registerUsageCollector(pluginsSetup.usageCollection); registerServerRoutes({ core: coreSetup, diff --git a/x-pack/plugins/observability_solution/investigate_app/server/types.ts b/x-pack/plugins/observability_solution/investigate_app/server/types.ts index fa4db6ccfcb05..8803221000d5b 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/types.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/types.ts @@ -10,6 +10,7 @@ import { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract, } from '@kbn/rule-registry-plugin/server'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; /* eslint-disable @typescript-eslint/no-empty-interface*/ @@ -18,6 +19,7 @@ export interface ConfigSchema {} export interface InvestigateAppSetupDependencies { observability: ObservabilityPluginSetup; ruleRegistry: RuleRegistryPluginSetupContract; + usageCollection: UsageCollectionSetup; } export interface InvestigateAppStartDependencies { diff --git a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json index a853456b1156c..d3974c0c0ed49 100644 --- a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json @@ -69,5 +69,6 @@ "@kbn/charts-plugin", "@kbn/observability-utils", "@kbn/observability-alerting-rule-utils", + "@kbn/usage-collection-plugin", ], } diff --git a/x-pack/plugins/observability_solution/observability/server/errors/errors.ts b/x-pack/plugins/observability_solution/observability/server/errors/errors.ts deleted file mode 100644 index eaec36e66d08b..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/errors/errors.ts +++ /dev/null @@ -1,26 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable max-classes-per-file */ - -export class ObservabilityError extends Error { - constructor(message?: string) { - super(message); - this.name = this.constructor.name; - } -} - -export class SLONotFound extends ObservabilityError {} -export class SLOIdConflict extends ObservabilityError {} - -export class InvalidQueryError extends ObservabilityError {} -export class InternalQueryError extends ObservabilityError {} -export class NotSupportedError extends ObservabilityError {} -export class IllegalArgumentError extends ObservabilityError {} -export class InvalidTransformError extends ObservabilityError {} - -export class SecurityException extends ObservabilityError {} diff --git a/x-pack/plugins/observability_solution/observability/server/errors/handler.ts b/x-pack/plugins/observability_solution/observability/server/errors/handler.ts deleted file mode 100644 index c10f1d98c083e..0000000000000 --- a/x-pack/plugins/observability_solution/observability/server/errors/handler.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ObservabilityError, SecurityException, SLOIdConflict, SLONotFound } from './errors'; - -export function getHTTPResponseCode(error: ObservabilityError): number { - if (error instanceof SLONotFound) { - return 404; - } - - if (error instanceof SLOIdConflict) { - return 409; - } - - if (error instanceof SecurityException) { - return 403; - } - - return 400; -} diff --git a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts index b885050eb64c7..5599039a5ce67 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts @@ -8,20 +8,19 @@ import { errors } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server'; import { IoTsParamsObject, decodeRequestParams, - stripNullishRequestParameters, parseEndpoint, passThroughValidationObject, + stripNullishRequestParameters, } from '@kbn/server-route-repository'; import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import axios from 'axios'; import * as t from 'io-ts'; -import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { ObservabilityConfig } from '..'; -import { getHTTPResponseCode, ObservabilityError } from '../errors'; import { AlertDetailsContextualInsightsService } from '../services'; import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; @@ -88,16 +87,6 @@ export function registerRoutes({ config, repository, core, logger, dependencies return response.ok({ body: data }); } catch (error) { - if (error instanceof ObservabilityError) { - logger.error(error.message); - return response.customError({ - statusCode: getHTTPResponseCode(error), - body: { - message: error.message, - }, - }); - } - if (axios.isAxiosError(error)) { logger.error(error); return response.customError({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/execute_connector.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/execute_connector.ts index 0088e35a6f6af..70f4ead5e6bb6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/execute_connector.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/execute_connector.ts @@ -8,13 +8,15 @@ import { FunctionRegistrationParameters } from '.'; import { FunctionVisibility } from '../../common'; +export const EXECUTE_CONNECTOR_FUNCTION_NAME = 'execute_connector'; + export function registerExecuteConnectorFunction({ functions, resources, }: FunctionRegistrationParameters) { functions.registerFunction( { - name: 'execute_connector', + name: EXECUTE_CONNECTOR_FUNCTION_NAME, description: 'Use this function when user explicitly asks to call a kibana connector.', visibility: FunctionVisibility.AssistantOnly, parameters: { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts index 59b883fef9c18..33f3bdd2c98f8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts @@ -23,7 +23,6 @@ import { JiraParamsSchema, PagerdutyParamsSchema, SlackApiParamsSchema, - SlackParamsSchema, WebhookParamsSchema, } from '@kbn/stack-connectors-plugin/server'; import { ObservabilityAIAssistantRouteHandlerResources } from '@kbn/observability-ai-assistant-plugin/server/routes/types'; @@ -37,14 +36,26 @@ import { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/com import { AlertDetailsContextualInsightsService } from '@kbn/observability-plugin/server/services'; import { getSystemMessageFromInstructions } from '@kbn/observability-ai-assistant-plugin/server/service/util/get_system_message_from_instructions'; import { AdHocInstruction } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { EXECUTE_CONNECTOR_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/server/functions/execute_connector'; import { convertSchemaToOpenApi } from './convert_schema_to_open_api'; import { OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID } from '../../common/rule_connector'; const CONNECTOR_PRIVILEGES = ['api:observabilityAIAssistant', 'app:observabilityAIAssistant']; const connectorParamsSchemas: Record = { + '.slack': { + type: 'object', + properties: { + id: { type: 'string' }, + params: { + type: 'object', + properties: { + message: { type: 'string' }, + }, + }, + }, + }, '.slack_api': convertSchemaToOpenApi(SlackApiParamsSchema), - '.slack': convertSchemaToOpenApi(SlackParamsSchema), '.email': convertSchemaToOpenApi(EmailParamsSchema), '.webhook': convertSchemaToOpenApi(WebhookParamsSchema), '.jira': convertSchemaToOpenApi(JiraParamsSchema), @@ -189,6 +200,24 @@ If available, include the link of the conversation at the end of your answer.` ), }; + const hasSlackConnector = !!connectorsList.filter( + (connector) => connector.actionTypeId === '.slack' + ).length; + + if (hasSlackConnector && functionClient.hasFunction(EXECUTE_CONNECTOR_FUNCTION_NAME)) { + const slackConnectorInstruction: AdHocInstruction = { + instruction_type: 'application_instruction', + text: dedent( + `The execute_connector function can be used to invoke Kibana connectors. + To send to the Slack connector, you need the following arguments: + - the "id" of the connector + - the "params" parameter that you will fill with the message + Please include both "id" and "params.message" in the function arguments when executing the Slack connector..` + ), + }; + functionClient.registerAdhocInstruction(slackConnectorInstruction); + } + const alertsContext = await getAlertsContext( execOptions.params.rule, execOptions.params.alerts, diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts index f89c6eb47e6c4..380bc3c3c5a26 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/translations.ts @@ -22,14 +22,26 @@ export const observabilityAppTitle = i18n.translate( } ); -export const betaBadgeTitle = i18n.translate('xpack.observabilityLogsExplorer.betaBadgeTitle', { - defaultMessage: 'Beta', -}); +export const deprecationBadgeTitle = i18n.translate( + 'xpack.observabilityLogsExplorer.deprecationBadgeTitle', + { + defaultMessage: 'Deprecation notice', + } +); + +export const deprecationBadgeDescription = i18n.translate( + 'xpack.observabilityLogsExplorer.deprecationBadgeDescription', + { + defaultMessage: + 'Logs Stream and Logs Explorer are set to be deprecated. Switch to Discover which now includes their functionality plus more features and better performance.', + } +); -export const betaBadgeDescription = i18n.translate( - 'xpack.observabilityLogsExplorer.betaBadgeDescription', +export const deprecationBadgeGuideline = i18n.translate( + 'xpack.observabilityLogsExplorer.deprecationBadgeGuideline', { - defaultMessage: 'This application is in beta and therefore subject to change.', + defaultMessage: + 'You can temporarily access Logs Stream by re-enabling it in Advanced Settings.', } ); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx index c3f91b3bf8660..0020df30b8708 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx @@ -21,7 +21,11 @@ import { LogsExplorerTabs } from '@kbn/discover-plugin/public'; import React, { useEffect, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { filter, take } from 'rxjs'; -import { betaBadgeDescription, betaBadgeTitle } from '../../common/translations'; +import { + deprecationBadgeDescription, + deprecationBadgeGuideline, + deprecationBadgeTitle, +} from '../../common/translations'; import { useKibanaContextForPlugin } from '../utils/use_kibana'; import { ConnectedDiscoverLink } from './discover_link'; import { FeedbackLink } from './feedback_link'; @@ -59,13 +63,7 @@ const ProjectTopNav = () => { `} > - + @@ -115,15 +113,6 @@ const ClassicTopNav = () => { margin-left: ${euiThemeVars.euiSizeM}; `} > - - - @@ -145,6 +134,9 @@ const ClassicTopNav = () => { + + + @@ -171,3 +163,19 @@ const ConditionalVerticalRule = ({ Component }: { Component: JSX.Element | null {Component} ); + +const DeprecationNoticeBadge = () => ( + + {deprecationBadgeDescription} +
+
+ {deprecationBadgeGuideline} + + } + alignment="middle" + /> +); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts index b1bbfb1b504a1..7106395d47b27 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts @@ -25,7 +25,7 @@ import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/publi import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import { LensPublicStart } from '@kbn/lens-plugin/public'; -import { SloPublicStart } from '@kbn/slo-plugin/public'; +import { SLOPublicStart } from '@kbn/slo-plugin/public'; import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import { ObservabilityLogsExplorerLocators, @@ -53,7 +53,7 @@ export interface ObservabilityLogsExplorerStartDeps { logsDataAccess: LogsDataAccessPluginStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; observabilityShared: ObservabilitySharedPluginStart; - slo: SloPublicStart; + slo: SLOPublicStart; serverless?: ServerlessPluginStart; triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; unifiedSearch?: UnifiedSearchPublicPluginStart; diff --git a/x-pack/plugins/observability_solution/slo/common/config.ts b/x-pack/plugins/observability_solution/slo/common/config.ts index 973fc562d743b..86fa7b74c2a1f 100644 --- a/x-pack/plugins/observability_solution/slo/common/config.ts +++ b/x-pack/plugins/observability_solution/slo/common/config.ts @@ -25,5 +25,5 @@ export const config = { experimental: true, }, }; -export type SloConfig = TypeOf; -export type ExperimentalFeatures = SloConfig['experimental']; +export type SLOConfig = TypeOf; +export type ExperimentalFeatures = SLOConfig['experimental']; diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json index b8d3e28ce210a..b4d66229dc495 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json @@ -559,7 +559,7 @@ } ], "responses": { - "200": { + "204": { "description": "Successful request" }, "400": { @@ -625,7 +625,7 @@ } ], "responses": { - "204": { + "200": { "description": "Successful request", "content": { "application/json": { diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml index dc57f3e4ea4f6..fde29b3602be0 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml @@ -341,7 +341,7 @@ paths: - $ref: '#/components/parameters/space_id' - $ref: '#/components/parameters/slo_id' responses: - '200': + '204': description: Successful request '400': description: Bad request @@ -380,7 +380,7 @@ paths: - $ref: '#/components/parameters/space_id' - $ref: '#/components/parameters/slo_id' responses: - '204': + '200': description: Successful request content: application/json: diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml index e805c1117f5c1..53d9e03e463e6 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@_reset.yaml @@ -11,7 +11,7 @@ post: - $ref: ../components/parameters/space_id.yaml - $ref: ../components/parameters/slo_id.yaml responses: - '204': + '200': description: Successful request content: application/json: diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@disable.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@disable.yaml index 704e2f8d24359..0a5194a550aa9 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@disable.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@{sloid}@disable.yaml @@ -11,7 +11,7 @@ post: - $ref: ../components/parameters/space_id.yaml - $ref: ../components/parameters/slo_id.yaml responses: - '200': + '204': description: Successful request '400': description: Bad request diff --git a/x-pack/plugins/observability_solution/slo/kibana.jsonc b/x-pack/plugins/observability_solution/slo/kibana.jsonc index c1054089c508a..e5732ee25e7e1 100644 --- a/x-pack/plugins/observability_solution/slo/kibana.jsonc +++ b/x-pack/plugins/observability_solution/slo/kibana.jsonc @@ -38,14 +38,14 @@ "presentationUtil", "features", "licensing", - "usageCollection" + "usageCollection", ], "optionalPlugins": [ "cloud", - "spaces", "serverless", "discover", - "observabilityAIAssistant" + "observabilityAIAssistant", + "spaces", ], "requiredBundles": [ "controls", diff --git a/x-pack/plugins/observability_solution/slo/public/application.tsx b/x-pack/plugins/observability_solution/slo/public/application.tsx index b6291b5286148..79160de114cd5 100644 --- a/x-pack/plugins/observability_solution/slo/public/application.tsx +++ b/x-pack/plugins/observability_solution/slo/public/application.tsx @@ -25,26 +25,20 @@ import { ExperimentalFeatures } from '../common/config'; import { PluginContext } from './context/plugin_context'; import { usePluginContext } from './hooks/use_plugin_context'; import { getRoutes } from './routes/routes'; -import { SloPublicPluginsStart } from './types'; +import { SLORepositoryClient, SLOPublicPluginsStart } from './types'; -function App() { - const { isServerless } = usePluginContext(); - - const routes = getRoutes(isServerless); - - return ( - <> - - {Object.keys(routes).map((path) => { - const { handler, exact } = routes[path]; - const Wrapper = () => { - return handler(); - }; - return ; - })} - - - ); +interface Props { + core: CoreStart; + plugins: SLOPublicPluginsStart; + appMountParameters: AppMountParameters; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; + ObservabilityPageTemplate: React.ComponentType; + usageCollection: UsageCollectionSetup; + isDev?: boolean; + kibanaVersion: string; + isServerless?: boolean; + experimentalFeatures: ExperimentalFeatures; + sloClient: SLORepositoryClient; } export const renderApp = ({ @@ -58,18 +52,8 @@ export const renderApp = ({ isServerless, observabilityRuleTypeRegistry, experimentalFeatures, -}: { - core: CoreStart; - plugins: SloPublicPluginsStart; - appMountParameters: AppMountParameters; - observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; - ObservabilityPageTemplate: React.ComponentType; - usageCollection: UsageCollectionSetup; - isDev?: boolean; - kibanaVersion: string; - isServerless?: boolean; - experimentalFeatures: ExperimentalFeatures; -}) => { + sloClient, +}: Props) => { const { element, history, theme$ } = appMountParameters; const isDarkMode = core.theme.getTheme().darkMode; @@ -128,6 +112,7 @@ export const renderApp = ({ ObservabilityPageTemplate, observabilityRuleTypeRegistry, experimentalFeatures, + sloClient, }} > @@ -160,3 +145,21 @@ export const renderApp = ({ ReactDOM.unmountComponentAtNode(element); }; }; + +function App() { + const { isServerless } = usePluginContext(); + + const routes = getRoutes(isServerless); + + return ( + + {Object.keys(routes).map((path) => { + const { handler, exact } = routes[path]; + const Wrapper = () => { + return handler(); + }; + return ; + })} + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/components/good_bad_events_chart/good_bad_events_chart.tsx b/x-pack/plugins/observability_solution/slo/public/components/good_bad_events_chart/good_bad_events_chart.tsx index ba3a201402e4f..31245a06b56d0 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/good_bad_events_chart/good_bad_events_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/good_bad_events_chart/good_bad_events_chart.tsx @@ -24,7 +24,7 @@ import React, { useRef } from 'react'; import { useAnnotations } from '@kbn/observability-plugin/public'; import { TimeBounds } from '../../pages/slo_details/types'; import { getBrushTimeBounds } from '../../utils/slo/duration'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { openInDiscover } from '../../utils/slo/get_discover_link'; export interface Props { diff --git a/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx b/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx index 1269dfb73afdb..bc01ed55acbdf 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/header_menu/header_menu.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui'; import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { SLOS_BASE_PATH, SLO_SETTINGS_PATH } from '../../../common/locators/paths'; diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx index 44ab29e77e1bd..49c9da82e9511 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { EuiFlexGroup, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AlertDetailsAppSectionProps } from '@kbn/observability-plugin/public'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { useFetchSloDetails } from '../../../../hooks/use_fetch_slo_details'; import { CustomAlertDetailsPanel } from './components/custom_panels/custom_panels'; import { ErrorRatePanel } from './components/error_rate/error_rate_panel'; diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/custom_panels/custom_kql/log_rate_analysis_panel.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/custom_panels/custom_kql/log_rate_analysis_panel.tsx index e94b8d5baed55..4a43fb85be824 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/custom_panels/custom_kql/log_rate_analysis_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/custom_panels/custom_kql/log_rate_analysis_panel.tsx @@ -27,7 +27,7 @@ import type { WindowSchema } from '../../../../../../../typings'; import { TimeRange } from '../../../../../error_rate_chart/use_lens_definition'; import { BurnRateAlert, BurnRateRule } from '../../../types'; import { getActionGroupFromReason } from '../../../utils/alert'; -import { useKibana } from '../../../../../../../utils/kibana_react'; +import { useKibana } from '../../../../../../../hooks/use_kibana'; import { getESQueryForLogRateAnalysis } from './helpers/log_rate_analysis_query'; function getDataTimeRange( timeRange: { gte: string; lte?: string }, diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx index 560f3f463bb46..1c371ea1abd30 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/components/error_rate/error_rate_panel.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { ALERT_EVALUATION_VALUE, ALERT_TIME_RANGE } from '@kbn/rule-data-utils'; import { GetSLOResponse } from '@kbn/slo-schema'; import React from 'react'; -import { useKibana } from '../../../../../../utils/kibana_react'; +import { useKibana } from '../../../../../../hooks/use_kibana'; import { ErrorRateChart } from '../../../../error_rate_chart'; import { TimeRange } from '../../../../error_rate_chart/use_lens_definition'; import { BurnRateAlert } from '../../types'; diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx index 261359e1b8873..fec552354102e 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx @@ -11,7 +11,7 @@ import moment from 'moment'; import React from 'react'; import { SloTabId } from '../../../pages/slo_details/components/slo_details'; import { TimeBounds } from '../../../pages/slo_details/types'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { getDelayInSecondsFromSLO } from '../../../utils/slo/get_delay_in_seconds_from_slo'; import { AlertAnnotation, TimeRange, useLensDefinition } from './use_lens_definition'; diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_outdated_callout/index.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_outdated_callout/index.tsx index 8495c43d677b3..e6b235898fa74 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_outdated_callout/index.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_outdated_callout/index.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { useFetchSloDefinitions } from '../../../hooks/use_fetch_slo_definitions'; import { paths } from '../../../../common/locators/paths'; diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx index 3c01e57faa398..bfa15ce490ab4 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/slo_status_badge/slo_active_alerts_badge.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { MouseEvent } from 'react'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { observabilityPaths } from '@kbn/observability-plugin/common'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; export interface Props { viewMode?: 'compact' | 'default'; diff --git a/x-pack/plugins/observability_solution/slo/public/constants.ts b/x-pack/plugins/observability_solution/slo/public/constants.ts index 07b79e5505736..ecd07f6209943 100644 --- a/x-pack/plugins/observability_solution/slo/public/constants.ts +++ b/x-pack/plugins/observability_solution/slo/public/constants.ts @@ -5,4 +5,3 @@ * 2.0. */ export const SLO_LONG_REFETCH_INTERVAL = 60 * 1000; // 1 minute -export const SLO_SHORT_REFETCH_INTERVAL = 5 * 1000; // 5 seconds diff --git a/x-pack/plugins/observability_solution/slo/public/context/plugin_context.tsx b/x-pack/plugins/observability_solution/slo/public/context/plugin_context.tsx index b61ccb8085a12..c0818cdea116e 100644 --- a/x-pack/plugins/observability_solution/slo/public/context/plugin_context.tsx +++ b/x-pack/plugins/observability_solution/slo/public/context/plugin_context.tsx @@ -10,6 +10,7 @@ import type { AppMountParameters } from '@kbn/core/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public'; import { ExperimentalFeatures } from '../../common/config'; +import type { SLORepositoryClient } from '../types'; export interface PluginContextValue { isDev?: boolean; @@ -18,6 +19,7 @@ export interface PluginContextValue { observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; ObservabilityPageTemplate: React.ComponentType; experimentalFeatures?: ExperimentalFeatures; + sloClient: SLORepositoryClient; } export const PluginContext = createContext(null); diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx index 24c29a20f1e6f..c3a505463e885 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx @@ -5,27 +5,28 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { Router } from '@kbn/shared-ux-router'; -import { BehaviorSubject, Subject } from 'rxjs'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { + FetchContext, + fetch$, initializeTitles, useBatchedPublishingSubjects, - fetch$, - FetchContext, useFetchContext, } from '@kbn/presentation-publishing'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { Router } from '@kbn/shared-ux-router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createBrowserHistory } from 'history'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; +import React, { useEffect } from 'react'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { PluginContext } from '../../../context/plugin_context'; +import { SLOPublicPluginsStart, SLORepositoryClient } from '../../../types'; import { SLO_ALERTS_EMBEDDABLE_ID } from './constants'; -import { SloAlertsEmbeddableState, SloAlertsApi } from './types'; -import { SloPublicPluginsStart, SloPublicStart } from '../../../types'; import { SloAlertsWrapper } from './slo_alerts_wrapper'; +import { SloAlertsApi, SloAlertsEmbeddableState } from './types'; const history = createBrowserHistory(); const queryClient = new QueryClient(); @@ -34,10 +35,17 @@ export const getAlertsPanelTitle = () => defaultMessage: 'SLO Alerts', }); -export function getAlertsEmbeddableFactory( - getStartServices: StartServicesAccessor, - kibanaVersion: string -) { +export function getAlertsEmbeddableFactory({ + coreStart, + pluginsStart, + sloClient, + kibanaVersion, +}: { + coreStart: CoreStart; + pluginsStart: SLOPublicPluginsStart; + sloClient: SLORepositoryClient; + kibanaVersion: string; +}) { const factory: ReactEmbeddableFactory< SloAlertsEmbeddableState, SloAlertsEmbeddableState, @@ -48,15 +56,15 @@ export function getAlertsEmbeddableFactory( return state.rawState as SloAlertsEmbeddableState; }, buildEmbeddable: async (state, buildApi, uuid, parentApi) => { - const [coreStart, pluginStart] = await getStartServices(); - const deps = { ...coreStart, ...pluginStart }; + const deps = { ...coreStart, ...pluginsStart }; async function onEdit() { try { const { openSloConfiguration } = await import('./slo_alerts_open_configuration'); const result = await openSloConfiguration( coreStart, - pluginStart, + pluginsStart, + sloClient, api.getSloAlertsConfig() ); api.updateSloAlertsConfig(result); @@ -143,18 +151,28 @@ export function getAlertsEmbeddableFactory( kibanaVersion, }} > - - - - - + + + + + + + ); diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_open_configuration.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_open_configuration.tsx index 655ad9e3d35ab..c4a8c3886e14f 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_open_configuration.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/slo_alerts_open_configuration.tsx @@ -4,17 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; import type { CoreStart } from '@kbn/core/public'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { toMountPoint } from '@kbn/react-kibana-mount'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { SloPublicPluginsStart } from '../../..'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; +import { SLOPublicPluginsStart } from '../../..'; +import { PluginContext } from '../../../context/plugin_context'; +import { SLORepositoryClient } from '../../../types'; import { SloConfiguration } from './slo_configuration'; import type { EmbeddableSloProps } from './types'; + export async function openSloConfiguration( coreStart: CoreStart, - pluginStart: SloPublicPluginsStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient, initialState?: EmbeddableSloProps ): Promise { const { overlays } = coreStart; @@ -26,22 +30,31 @@ export async function openSloConfiguration( - - { - flyoutSession.close(); - resolve(update); - }} - onCancel={() => { - flyoutSession.close(); - reject(); - }} - /> - + + + { + flyoutSession.close(); + resolve(update); + }} + onCancel={() => { + flyoutSession.close(); + reject(); + }} + /> + + , coreStart ) diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx index d0370877b511e..5cceab9fa10d8 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx @@ -4,31 +4,40 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { Router } from '@kbn/shared-ux-router'; -import { createBrowserHistory } from 'history'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { + fetch$, initializeTitles, useBatchedPublishingSubjects, - fetch$, } from '@kbn/presentation-publishing'; -import { BehaviorSubject, Subject } from 'rxjs'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { Router } from '@kbn/shared-ux-router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { SLO_BURN_RATE_EMBEDDABLE_ID } from './constants'; -import { SloBurnRateEmbeddableState, SloEmbeddableDeps, BurnRateApi } from './types'; +import { createBrowserHistory } from 'history'; +import React, { useEffect } from 'react'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; import { BurnRate } from './burn_rate'; +import { SLO_BURN_RATE_EMBEDDABLE_ID } from './constants'; +import { BurnRateApi, SloBurnRateEmbeddableState } from './types'; +import type { SLOPublicPluginsStart, SLORepositoryClient } from '../../../types'; +import { PluginContext } from '../../../context/plugin_context'; -export const getTitle = () => +const getTitle = () => i18n.translate('xpack.slo.burnRateEmbeddable.title', { defaultMessage: 'SLO Burn Rate', }); -const queryClient = new QueryClient(); - -export const getBurnRateEmbeddableFactory = (deps: SloEmbeddableDeps) => { +export const getBurnRateEmbeddableFactory = ({ + coreStart, + pluginsStart, + sloClient, +}: { + coreStart: CoreStart; + pluginsStart: SLOPublicPluginsStart; + sloClient: SLORepositoryClient; +}) => { const factory: ReactEmbeddableFactory< SloBurnRateEmbeddableState, SloBurnRateEmbeddableState, @@ -39,6 +48,7 @@ export const getBurnRateEmbeddableFactory = (deps: SloEmbeddableDeps) => { return state.rawState as SloBurnRateEmbeddableState; }, buildEmbeddable: async (state, buildApi, uuid, parentApi) => { + const deps = { ...coreStart, ...pluginsStart }; const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); const defaultTitle$ = new BehaviorSubject(getTitle()); const sloId$ = new BehaviorSubject(state.sloId); @@ -84,18 +94,26 @@ export const getBurnRateEmbeddableFactory = (deps: SloEmbeddableDeps) => { duration$ ); - const I18nContext = deps.i18n.Context; - useEffect(() => { return () => { fetchSubscription.unsubscribe(); }; }, []); + const queryClient = new QueryClient(); + return ( - - - + + + { reloadSubject={reload$} /> - - - + + + ); }, }; diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/open_configuration.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/open_configuration.tsx index e8a7777b29a62..8a881b417bb64 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/open_configuration.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/burn_rate/open_configuration.tsx @@ -10,13 +10,16 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; -import { SloPublicPluginsStart } from '../../..'; +import { SLOPublicPluginsStart } from '../../..'; +import { PluginContext } from '../../../context/plugin_context'; +import { SLORepositoryClient } from '../../../types'; import { Configuration } from './configuration'; import type { EmbeddableProps, SloBurnRateEmbeddableState } from './types'; export async function openConfiguration( coreStart: CoreStart, - pluginStart: SloPublicPluginsStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient, initialState?: SloBurnRateEmbeddableState ): Promise { const { overlays } = coreStart; @@ -28,21 +31,30 @@ export async function openConfiguration( - - { - flyoutSession.close(); - resolve(update); - }} - onCancel={() => { - flyoutSession.close(); - reject(); - }} - /> - + + + { + flyoutSession.close(); + resolve(update); + }} + onCancel={() => { + flyoutSession.close(); + reject(); + }} + /> + + , coreStart ) diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_embeddable_context.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_embeddable_context.tsx deleted file mode 100644 index acb0897b07e71..0000000000000 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_embeddable_context.tsx +++ /dev/null @@ -1,43 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { Router } from '@kbn/shared-ux-router'; -import { createBrowserHistory } from 'history'; -import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { PluginContext } from '../../../context/plugin_context'; -import { SloEmbeddableDeps } from '../overview/types'; - -const queryClient = new QueryClient(); - -export interface SloEmbeddableContextProps { - deps: SloEmbeddableDeps; - children: React.ReactNode; -} - -export function SloEmbeddableContext({ deps, children }: SloEmbeddableContextProps) { - const { observabilityRuleTypeRegistry } = deps.observability; - const { navigation } = deps.observabilityShared; - - return ( - - - - - {children} - - - - - ); -} diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx index 5bfc07f3562bd..3cb5b18aab4a5 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -14,22 +13,23 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, - EuiTitle, - EuiTabs, - EuiTab, EuiSpacer, + EuiTab, + EuiTabs, + EuiTitle, } from '@elastic/eui'; -import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; -import { useKibana } from '../../../utils/kibana_react'; -import { useSloDetailsTabs } from '../../../pages/slo_details/hooks/use_slo_details_tabs'; +import React, { useState } from 'react'; import { HeaderTitle } from '../../../pages/slo_details/components/header_title'; -import { getSloFormattedSummary } from '../../../pages/slos/hooks/use_slo_summary'; import { OVERVIEW_TAB_ID, SloDetails, SloTabId, } from '../../../pages/slo_details/components/slo_details'; +import { useSloDetailsTabs } from '../../../pages/slo_details/hooks/use_slo_details_tabs'; +import { getSloFormattedSummary } from '../../../pages/slos/hooks/use_slo_summary'; +import { useKibana } from '../../../hooks/use_kibana'; export function SloOverviewDetails({ slo, diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_open_configuration.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_open_configuration.tsx index 6798b7b9c46a6..79e1e1eafe2a9 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_open_configuration.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_open_configuration.tsx @@ -4,17 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; import type { CoreStart } from '@kbn/core/public'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { toMountPoint } from '@kbn/react-kibana-mount'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { SloPublicPluginsStart } from '../../..'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; +import { SLOPublicPluginsStart } from '../../..'; +import { PluginContext } from '../../../context/plugin_context'; import { SloConfiguration } from './slo_configuration'; -import type { SloErrorBudgetEmbeddableState, EmbeddableSloProps } from './types'; +import type { EmbeddableSloProps, SloErrorBudgetEmbeddableState } from './types'; +import { SLORepositoryClient } from '../../../types'; + export async function openSloConfiguration( coreStart: CoreStart, - pluginStart: SloPublicPluginsStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient, initialState?: SloErrorBudgetEmbeddableState ): Promise { const { overlays } = coreStart; @@ -26,21 +30,30 @@ export async function openSloConfiguration( - - { - flyoutSession.close(); - resolve(update); - }} - onCancel={() => { - flyoutSession.close(); - reject(); - }} - /> - + + + { + flyoutSession.close(); + resolve(update); + }} + onCancel={() => { + flyoutSession.close(); + reject(); + }} + /> + + , coreStart ) diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx index 6d01995fb8191..b76152124825d 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx @@ -4,30 +4,40 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { Router } from '@kbn/shared-ux-router'; -import { createBrowserHistory } from 'history'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { + fetch$, initializeTitles, useBatchedPublishingSubjects, - fetch$, } from '@kbn/presentation-publishing'; -import { BehaviorSubject, Subject } from 'rxjs'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { Router } from '@kbn/shared-ux-router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createBrowserHistory } from 'history'; +import React, { useEffect } from 'react'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { PluginContext } from '../../../context/plugin_context'; +import { SLOPublicPluginsStart, SLORepositoryClient } from '../../../types'; import { SLO_ERROR_BUDGET_ID } from './constants'; -import { SloErrorBudgetEmbeddableState, SloEmbeddableDeps, ErrorBudgetApi } from './types'; import { SloErrorBudget } from './error_budget_burn_down'; +import { ErrorBudgetApi, SloErrorBudgetEmbeddableState } from './types'; -export const getErrorBudgetPanelTitle = () => +const getErrorBudgetPanelTitle = () => i18n.translate('xpack.slo.errorBudgetEmbeddable.title', { defaultMessage: 'SLO Error Budget burn down', }); -const queryClient = new QueryClient(); -export const getErrorBudgetEmbeddableFactory = (deps: SloEmbeddableDeps) => { +export const getErrorBudgetEmbeddableFactory = ({ + coreStart, + pluginsStart, + sloClient, +}: { + coreStart: CoreStart; + pluginsStart: SLOPublicPluginsStart; + sloClient: SLORepositoryClient; +}) => { const factory: ReactEmbeddableFactory< SloErrorBudgetEmbeddableState, SloErrorBudgetEmbeddableState, @@ -38,6 +48,7 @@ export const getErrorBudgetEmbeddableFactory = (deps: SloEmbeddableDeps) => { return state.rawState as SloErrorBudgetEmbeddableState; }, buildEmbeddable: async (state, buildApi, uuid, parentApi) => { + const deps = { ...coreStart, ...pluginsStart }; const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); const defaultTitle$ = new BehaviorSubject(getErrorBudgetPanelTitle()); const sloId$ = new BehaviorSubject(state.sloId); @@ -76,18 +87,26 @@ export const getErrorBudgetEmbeddableFactory = (deps: SloEmbeddableDeps) => { Component: () => { const [sloId, sloInstanceId] = useBatchedPublishingSubjects(sloId$, sloInstanceId$); - const I18nContext = deps.i18n.Context; - useEffect(() => { return () => { fetchSubscription.unsubscribe(); }; }, []); + const queryClient = new QueryClient(); + return ( - - - + + + { reloadSubject={reload$} /> - - - + + + ); }, }; diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/group_view/slo_group_filters.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/group_view/slo_group_filters.tsx index 2f32d360853bb..d9995adfc412c 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/group_view/slo_group_filters.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/group_view/slo_group_filters.tsx @@ -15,8 +15,8 @@ import { SLO_SUMMARY_DESTINATION_INDEX_NAME } from '../../../../../common/consta import { useCreateDataView } from '../../../../hooks/use_create_data_view'; import { useFetchSloGroups } from '../../../../hooks/use_fetch_slo_groups'; import { SLI_OPTIONS } from '../../../../pages/slo_edit/constants'; -import { useGetSettings } from '../../../../pages/slo_settings/use_get_settings'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useGetSettings } from '../../../../pages/slo_settings/hooks/use_get_settings'; +import { useKibana } from '../../../../hooks/use_kibana'; import type { GroupBy, GroupFilters } from '../types'; import './slo_group_filters.scss'; diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx index 7704bc5326310..e74ba591e7166 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx @@ -5,33 +5,45 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import styled from 'styled-components'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { + fetch$, initializeTitles, useBatchedPublishingSubjects, - fetch$, } from '@kbn/presentation-publishing'; +import { Router } from '@kbn/shared-ux-router'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createBrowserHistory } from 'history'; +import React, { useEffect } from 'react'; import { BehaviorSubject, Subject } from 'rxjs'; -import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; +import styled from 'styled-components'; +import { PluginContext } from '../../../context/plugin_context'; +import type { SLOPublicPluginsStart, SLORepositoryClient } from '../../../types'; import { SLO_OVERVIEW_EMBEDDABLE_ID } from './constants'; -import { SloCardChartList } from './slo_overview_grid'; -import { SloOverview } from './slo_overview'; import { GroupSloView } from './group_view/group_view'; -import { SloOverviewEmbeddableState, SloOverviewApi, GroupSloCustomInput } from './types'; -import { SloPublicPluginsStart, SloPublicStart } from '../../../types'; -import { SloEmbeddableContext } from '../common/slo_embeddable_context'; +import { SloOverview } from './slo_overview'; +import { SloCardChartList } from './slo_overview_grid'; +import { GroupSloCustomInput, SloOverviewApi, SloOverviewEmbeddableState } from './types'; -export const getOverviewPanelTitle = () => +const getOverviewPanelTitle = () => i18n.translate('xpack.slo.sloEmbeddable.displayName', { defaultMessage: 'SLO Overview', }); -export const getOverviewEmbeddableFactory = ( - getStartServices: StartServicesAccessor -) => { + +export const getOverviewEmbeddableFactory = ({ + coreStart, + pluginsStart, + sloClient, +}: { + coreStart: CoreStart; + pluginsStart: SLOPublicPluginsStart; + sloClient: SLORepositoryClient; +}) => { const factory: ReactEmbeddableFactory< SloOverviewEmbeddableState, SloOverviewEmbeddableState, @@ -42,15 +54,15 @@ export const getOverviewEmbeddableFactory = ( return state.rawState as SloOverviewEmbeddableState; }, buildEmbeddable: async (state, buildApi, uuid, parentApi) => { - const [coreStart, pluginStart] = await getStartServices(); - const deps = { ...coreStart, ...pluginStart }; + const deps = { ...coreStart, ...pluginsStart }; async function onEdit() { try { const { openSloConfiguration } = await import('./slo_overview_open_configuration'); const result = await openSloConfiguration( coreStart, - pluginStart, + pluginsStart, + sloClient, api.getSloGroupOverviewConfig() ); api.updateSloGroupOverviewConfig(result as GroupSloCustomInput); @@ -184,10 +196,33 @@ export const getOverviewEmbeddableFactory = ( ); } }; + + const queryClient = new QueryClient(); + return ( - - {showAllGroupByInstances ? : renderOverview()} - + + + + + + {showAllGroupByInstances ? ( + + ) : ( + renderOverview() + )} + + + + + ); }, }; diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx index f452f77cb1da3..88e10faeae1a1 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_grid.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { SloOverviewDetails } from '../common/slo_overview_details'; import { useFetchSloList } from '../../../hooks/use_fetch_slo_list'; import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter'; diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_open_configuration.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_open_configuration.tsx index 7d10a0ca76bfb..de4f248aad0bf 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_open_configuration.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/slo_overview_open_configuration.tsx @@ -11,15 +11,21 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { GroupSloCustomInput, SingleSloCustomInput } from './types'; -import { SloPublicPluginsStart } from '../../..'; +import { SLOPublicPluginsStart } from '../../..'; import { SloConfiguration } from './slo_configuration'; +import { SLORepositoryClient } from '../../../types'; +import { PluginContext } from '../../../context/plugin_context'; + export async function openSloConfiguration( coreStart: CoreStart, - pluginStart: SloPublicPluginsStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient, initialState?: GroupSloCustomInput ): Promise { const { overlays } = coreStart; + const queryClient = new QueryClient(); + return new Promise(async (resolve, reject) => { try { const flyoutSession = overlays.openFlyout( @@ -27,22 +33,31 @@ export async function openSloConfiguration( - - { - flyoutSession.close(); - resolve(update); - }} - onCancel={() => { - flyoutSession.close(); - reject(); - }} - /> - + + + { + flyoutSession.close(); + resolve(update); + }} + onCancel={() => { + flyoutSession.close(); + reject(); + }} + /> + + , coreStart ) diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts index c64faff1f110d..3c2866077aaa6 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/overview/types.ts @@ -13,15 +13,6 @@ import { import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; import { Filter } from '@kbn/es-query'; -import { - type CoreStart, - IUiSettingsClient, - ApplicationStart, - NotificationsStart, -} from '@kbn/core/public'; -import { ObservabilityPublicStart } from '@kbn/observability-plugin/public'; -import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; export type OverviewMode = 'single' | 'groups'; export type GroupBy = 'slo.tags' | 'status' | 'slo.indicator.type'; @@ -72,18 +63,6 @@ export const apiHasSloGroupOverviewConfig = ( ); }; -export interface SloEmbeddableDeps { - uiSettings: IUiSettingsClient; - http: CoreStart['http']; - i18n: CoreStart['i18n']; - theme: CoreStart['theme']; - application: ApplicationStart; - notifications: NotificationsStart; - observability: ObservabilityPublicStart; - observabilityShared: ObservabilitySharedPluginStart; - uiActions: UiActionsStart; -} - export type SloOverviewEmbeddableActionContext = EmbeddableApiContext & { embeddable: SloOverviewApi; }; diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/types.ts b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/types.ts new file mode 100644 index 0000000000000..08397cd7e6817 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/types.ts @@ -0,0 +1,30 @@ +/* + * 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 { + ApplicationStart, + CoreStart, + IUiSettingsClient, + NotificationsStart, +} from '@kbn/core/public'; +import { ObservabilityPublicStart } from '@kbn/observability-plugin/public'; +import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; +import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { SLORepositoryClient } from '../../types'; + +export interface SLOEmbeddableDeps { + uiSettings: IUiSettingsClient; + http: CoreStart['http']; + i18n: CoreStart['i18n']; + theme: CoreStart['theme']; + application: ApplicationStart; + notifications: NotificationsStart; + observability: ObservabilityPublicStart; + observabilityShared: ObservabilitySharedPluginStart; + uiActions: UiActionsStart; + sloClient: SLORepositoryClient; +} diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_alerts_url.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_alerts_url.ts index fa97a22204372..65c71a6b58973 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_alerts_url.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_alerts_url.ts @@ -7,7 +7,7 @@ import { observabilityPaths } from '@kbn/observability-plugin/common'; import rison from '@kbn/rison'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; export const useAlertsUrl = () => { const { basePath } = useKibana().services.http; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_capabilities.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_capabilities.ts index bc1856681b930..52db1245b5608 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_capabilities.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_capabilities.ts @@ -5,7 +5,7 @@ * 2.0. */ import { sloFeatureId } from '@kbn/observability-plugin/common'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; export function useCapabilities() { const { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts index dc7901e81b528..fbb4145b6bec9 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_clone_slo.ts @@ -9,7 +9,7 @@ import { encode } from '@kbn/rison'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useCallback } from 'react'; import { paths } from '../../common/locators/paths'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { createRemoteSloCloneUrl } from '../utils/slo/remote_slo_urls'; import { useSpace } from './use_space'; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_burn_rate_rule.tsx b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_burn_rate_rule.tsx index 906e844c14806..3fa2b7f8ffd81 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_burn_rate_rule.tsx +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_burn_rate_rule.tsx @@ -16,7 +16,7 @@ import type { } from '@kbn/alerting-plugin/common/routes/rule/apis/create'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; export function useCreateRule() { const { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts index 781145885df5f..d630ce24c4751 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_data_view.ts @@ -6,7 +6,7 @@ */ import { useFetcher } from '@kbn/observability-shared-plugin/public'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; interface UseCreateDataViewProps { indexPatternString?: string; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx index e5b6d2c114a84..2059c9b273592 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_create_slo.tsx @@ -15,9 +15,10 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { paths } from '../../common/locators/paths'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; type ServerError = IHttpFetchError; @@ -29,6 +30,7 @@ export function useCreateSlo() { http, notifications: { toasts }, } = useKibana().services; + const { sloClient } = usePluginContext(); const services = useKibana().services; const queryClient = useQueryClient(); @@ -40,8 +42,7 @@ export function useCreateSlo() { >( ['createSlo'], ({ slo }) => { - const body = JSON.stringify(slo); - return http.post(`/api/observability/slos`, { body }); + return sloClient.fetch(`POST /api/observability/slos 2023-10-31`, { params: { body: slo } }); }, { onSuccess: (data, { slo }) => { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo.ts index a8d2758e4a9c2..237c5f909ae8a 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo.ts @@ -8,23 +8,26 @@ import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; type ServerError = IHttpFetchError; export function useDeleteSlo() { const { - http, notifications: { toasts }, } = useKibana().services; + const { sloClient } = usePluginContext(); const queryClient = useQueryClient(); - return useMutation( + return useMutation( ['deleteSlo'], ({ id }) => { try { - return http.delete(`/api/observability/slos/${id}`); + return sloClient.fetch(`DELETE /api/observability/slos/{id} 2023-10-31`, { + params: { path: { id } }, + }); } catch (error) { return Promise.reject(`Something went wrong: ${String(error)}`); } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo_instance.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo_instance.ts index 66f0012659446..9b84e6f565ead 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo_instance.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_delete_slo_instance.ts @@ -7,34 +7,37 @@ import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; type ServerError = IHttpFetchError; export function useDeleteSloInstance() { const { - http, notifications: { toasts }, } = useKibana().services; + const { sloClient } = usePluginContext(); const queryClient = useQueryClient(); - return useMutation( + return useMutation( ['deleteSloInstance'], ({ slo, excludeRollup }) => { try { - return http.post(`/api/observability/slos/_delete_instances`, { - body: JSON.stringify({ - list: [ - { - sloId: slo.id, - instanceId: slo.instanceId, - excludeRollup, - }, - ], - }), + return sloClient.fetch(`POST /api/observability/slos/_delete_instances 2023-10-31`, { + params: { + body: { + list: [ + { + sloId: slo.id, + instanceId: slo.instanceId ?? ALL_VALUE, + excludeRollup, + }, + ], + }, + }, }); } catch (error) { return Promise.reject(`Something went wrong: ${String(error)}`); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts index 6ad34d8c4dc86..8fa7d3ec88e91 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_active_alerts.ts @@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; import { AlertConsumers } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; import { ActiveAlerts } from './active_alerts'; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_indices.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_indices.ts index e21df7b56a964..436ba45fad273 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_indices.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_indices.ts @@ -7,7 +7,7 @@ import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; type ApmIndex = string; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_suggestions.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_suggestions.ts index c6484076b1d72..ea8ae831a26cf 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_suggestions.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_apm_suggestions.ts @@ -7,7 +7,7 @@ import { useQuery } from '@tanstack/react-query'; import moment from 'moment'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; export type Suggestion = string; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_global_diagnosis.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_global_diagnosis.ts index df8ea83ed2aaa..d943322cf1322 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_global_diagnosis.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_global_diagnosis.ts @@ -9,9 +9,10 @@ import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/a import { i18n } from '@kbn/i18n'; import type { PublicLicenseJSON } from '@kbn/licensing-plugin/public'; import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { convertErrorForUseInToast } from './helpers/convert_error_for_use_in_toast'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; interface SloGlobalDiagnosisResponse { licenseAndFeatures: PublicLicenseJSON; @@ -25,23 +26,17 @@ export interface UseFetchSloGlobalDiagnoseResponse { export function useFetchSloGlobalDiagnosis(): UseFetchSloGlobalDiagnoseResponse { const { - http, notifications: { toasts }, } = useKibana().services; + const { sloClient } = usePluginContext(); const { isLoading, data } = useQuery({ queryKey: sloKeys.globalDiagnosis(), queryFn: async ({ signal }) => { try { - const response = await http.get( - '/internal/observability/slos/_diagnosis', - { - query: {}, - signal, - } - ); - - return response; + return await sloClient.fetch('GET /internal/observability/slos/_diagnosis', { + signal, + }); } catch (error) { throw convertErrorForUseInToast(error); } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_group_by_cardinality.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_group_by_cardinality.ts index 6be95e67c0d89..beab209f7c692 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_group_by_cardinality.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_group_by_cardinality.ts @@ -10,7 +10,7 @@ import { ALL_VALUE, QuerySchema } from '@kbn/slo-schema'; import { useQuery } from '@tanstack/react-query'; import { lastValueFrom } from 'rxjs'; import { getElasticsearchQueryOrThrow } from '../../common/parse_kuery'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; export interface UseFetchGroupByCardinalityResponse { isLoading: boolean; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts index fc77e0ba40c7a..b8b0bc7ca9c4e 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { useQuery } from '@tanstack/react-query'; import { ALL_VALUE, FetchHistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; -import { useKibana } from '../utils/kibana_react'; -import { sloKeys } from './query_key_factory'; +import { useQuery } from '@tanstack/react-query'; import { SLO_LONG_REFETCH_INTERVAL } from '../constants'; +import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; export interface UseFetchHistoricalSummaryResponse { data: FetchHistoricalSummaryResponse | undefined; @@ -34,7 +34,7 @@ export function useFetchHistoricalSummary({ shouldRefetch, range, }: Params): UseFetchHistoricalSummaryResponse { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const list = sloList.map((slo) => ({ sloId: slo.id, @@ -57,15 +57,10 @@ export function useFetchHistoricalSummary({ queryKey: sloKeys.historicalSummary(list), queryFn: async ({ signal }) => { try { - const response = await http.post( - '/internal/observability/slos/_historical_summary', - { - body: JSON.stringify({ list }), - signal, - } - ); - - return response; + return await sloClient.fetch('POST /internal/observability/slos/_historical_summary', { + params: { body: { list } }, + signal, + }); } catch (error) { // ignore error } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts index 51a8337e4dd82..841bc9bec0a47 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_rules_for_slo.ts @@ -9,7 +9,7 @@ import type { Rule, AsApiContract } from '@kbn/triggers-actions-ui-plugin/public import { transformRule } from '@kbn/triggers-actions-ui-plugin/public'; import { useQuery } from '@tanstack/react-query'; import { BurnRateRuleParams } from '../typings'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; interface Params { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts index 3da6e09072dfd..01404a6261a49 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_burn_rates.ts @@ -12,8 +12,8 @@ import { useQuery, } from '@tanstack/react-query'; import { SLO_LONG_REFETCH_INTERVAL } from '../constants'; -import { useKibana } from '../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; export interface UseFetchSloBurnRatesResponse { isLoading: boolean; @@ -34,19 +34,24 @@ export function useFetchSloBurnRates({ windows, shouldRefetch, }: UseFetchSloBurnRatesParams): UseFetchSloBurnRatesResponse { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const { isLoading, data, refetch } = useQuery({ queryKey: sloKeys.burnRates(slo.id, slo.instanceId, windows), queryFn: async ({ signal }) => { try { - const response = await http.post( - `/internal/observability/slos/${slo.id}/_burn_rates`, + const response = await sloClient.fetch( + 'POST /internal/observability/slos/{id}/_burn_rates', { - body: JSON.stringify({ - windows, - instanceId: slo.instanceId ?? ALL_VALUE, - remoteName: slo.remote?.remoteName, - }), + params: { + path: { + id: slo.id, + }, + body: { + windows, + instanceId: slo.instanceId ?? ALL_VALUE, + remoteName: slo.remote?.remoteName, + }, + }, signal, } ); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_definitions.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_definitions.ts index 8aae512faab44..085cb777a32ec 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_definitions.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_definitions.ts @@ -7,8 +7,8 @@ import { FindSLODefinitionsResponse } from '@kbn/slo-schema'; import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; export interface UseFetchSloDefinitionsResponse { data: FindSLODefinitionsResponse | undefined; @@ -31,19 +31,19 @@ export function useFetchSloDefinitions({ page = 1, perPage = 100, }: Params): UseFetchSloDefinitionsResponse { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const search = name.endsWith('*') ? name : `${name}*`; const { isLoading, isError, isSuccess, data, refetch } = useQuery({ queryKey: sloKeys.definitions(search, page, perPage, includeOutdatedOnly), queryFn: async ({ signal }) => { try { - const response = await http.get( - '/api/observability/slos/_definitions', - { query: { search, includeOutdatedOnly, page, perPage }, signal } - ); - - return response; + return await sloClient.fetch('GET /api/observability/slos/_definitions 2023-10-31', { + params: { + query: { search, includeOutdatedOnly, page: String(page), perPage: String(perPage) }, + }, + signal, + }); } catch (error) { throw new Error(`Something went wrong. Error: ${error}`); } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts index 589701be319ae..b32c0c6dc5976 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_details.ts @@ -12,9 +12,9 @@ import { RefetchQueryFilters, useQuery, } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; import { SLO_LONG_REFETCH_INTERVAL } from '../constants'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; export interface UseFetchSloDetailsResponse { isInitialLoading: boolean; @@ -39,17 +39,20 @@ export function useFetchSloDetails({ remoteName?: string; shouldRefetch?: boolean; }): UseFetchSloDetailsResponse { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery( { queryKey: sloKeys.detail(sloId!, instanceId, remoteName), queryFn: async ({ signal }) => { try { - const response = await http.get(`/api/observability/slos/${sloId}`, { - query: { - ...(!!instanceId && instanceId !== ALL_VALUE && { instanceId }), - ...(remoteName && { remoteName }), + const response = await sloClient.fetch('GET /api/observability/slos/{id} 2023-10-31', { + params: { + path: { id: sloId! }, + query: { + ...(!!instanceId && instanceId !== ALL_VALUE && { instanceId }), + ...(remoteName && { remoteName }), + }, }, signal, }); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_groups.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_groups.ts index f7cdfbed28f3f..4b409a2c56562 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_groups.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_groups.ts @@ -4,25 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { Filter, buildQueryFromFilters } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { FindSLOGroupsResponse } from '@kbn/slo-schema'; import { - useQuery, - RefetchOptions, QueryObserverResult, + RefetchOptions, RefetchQueryFilters, + useQuery, } from '@tanstack/react-query'; -import { i18n } from '@kbn/i18n'; -import { buildQueryFromFilters, Filter } from '@kbn/es-query'; import { useMemo } from 'react'; -import { FindSLOGroupsResponse } from '@kbn/slo-schema'; -import { useKibana } from '../utils/kibana_react'; -import { useCreateDataView } from './use_create_data_view'; -import { sloKeys } from './query_key_factory'; import { DEFAULT_SLO_GROUPS_PAGE_SIZE, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, } from '../../common/constants'; -import { SearchState } from '../pages/slos/hooks/use_url_search_state'; import { GroupByField } from '../pages/slos/components/slo_list_group_by'; +import { SearchState } from '../pages/slos/hooks/use_url_search_state'; +import { useKibana } from './use_kibana'; +import { sloKeys } from './query_key_factory'; +import { useCreateDataView } from './use_create_data_view'; +import { usePluginContext } from './use_plugin_context'; interface SLOGroupsParams { page?: number; @@ -58,8 +59,8 @@ export function useFetchSloGroups({ filters: filterDSL = [], lastRefresh, }: SLOGroupsParams = {}): UseFetchSloGroupsResponse { + const { sloClient } = usePluginContext(); const { - http, notifications: { toasts }, } = useKibana().services; @@ -97,20 +98,19 @@ export function useFetchSloGroups({ lastRefresh, }), queryFn: async ({ signal }) => { - const response = await http.get( - '/internal/observability/slos/_groups', - { + const response = await sloClient.fetch('GET /internal/observability/slos/_groups', { + params: { query: { - ...(page && { page }), - ...(perPage && { perPage }), + ...(page && { page: String(page) }), + ...(perPage && { perPage: String(perPage) }), ...(groupBy && { groupBy }), ...(groupsFilter && { groupsFilter }), ...(kqlQuery && { kqlQuery }), ...(filters && { filters }), }, - signal, - } - ); + }, + signal, + }); return response; }, cacheTime: 0, diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_health.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_health.ts index 4d8439331d042..3dc0024082f1c 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_health.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_health.ts @@ -7,8 +7,8 @@ import { ALL_VALUE, FetchSLOHealthResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; export interface UseFetchSloHealth { data: FetchSLOHealthResponse | undefined; @@ -21,7 +21,7 @@ export interface Params { } export function useFetchSloHealth({ list }: Params): UseFetchSloHealth { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const payload = list.map((slo) => ({ sloId: slo.id, sloInstanceId: slo.instanceId ?? ALL_VALUE, @@ -31,15 +31,10 @@ export function useFetchSloHealth({ list }: Params): UseFetchSloHealth { queryKey: sloKeys.health(payload), queryFn: async ({ signal }) => { try { - const response = await http.post( - '/internal/observability/slos/_health', - { - body: JSON.stringify({ list: payload }), - signal, - } - ); - - return response; + return await sloClient.fetch('POST /internal/observability/slos/_health', { + params: { body: { list: payload } }, + signal, + }); } catch (error) { // ignore error } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts index 51308b23b3300..df360801ed6fd 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_inspect.ts @@ -5,39 +5,21 @@ * 2.0. */ -import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { CreateSLOInput, SLODefinitionResponse } from '@kbn/slo-schema'; +import type { CreateSLOInput } from '@kbn/slo-schema'; import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; - -interface SLOInspectResponse { - slo: SLODefinitionResponse; - rollUpPipeline: Record; - summaryPipeline: Record; - rollUpTransform: TransformPutTransformRequest; - summaryTransform: TransformPutTransformRequest; - temporaryDoc: Record; - rollUpTransformCompositeQuery: string; - summaryTransformCompositeQuery: string; -} +import { usePluginContext } from './use_plugin_context'; export function useFetchSloInspect(slo: CreateSLOInput, shouldInspect: boolean) { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const { isLoading, isError, isSuccess, data } = useQuery({ queryKey: ['slo', 'inspect'], queryFn: async ({ signal }) => { try { - const body = JSON.stringify(slo); - const response = await http.post( - '/internal/observability/slos/_inspect', - { - body, - signal, - } - ); - - return response; + return await sloClient.fetch('POST /internal/observability/slos/_inspect', { + params: { body: slo }, + signal, + }); } catch (error) { // ignore error } diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_list.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_list.ts index 0e2bb39dab878..aba2fe66dcb03 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_list.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_list.ts @@ -14,17 +14,17 @@ import { DEFAULT_SLO_PAGE_SIZE, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, } from '../../common/constants'; -import { SearchState } from '../pages/slos/hooks/use_url_search_state'; -import { useKibana } from '../utils/kibana_react'; -import { useCreateDataView } from './use_create_data_view'; - +import { SearchState, SortDirection, SortField } from '../pages/slos/hooks/use_url_search_state'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; +import { useCreateDataView } from './use_create_data_view'; +import { usePluginContext } from './use_plugin_context'; export interface SLOListParams { kqlQuery?: string; page?: number; - sortBy?: string; - sortDirection?: 'asc' | 'desc'; + sortBy?: SortField; + sortDirection?: SortDirection; perPage?: number; filters?: Filter[]; lastRefresh?: number; @@ -55,9 +55,10 @@ export function useFetchSloList({ disabled = false, }: SLOListParams = {}): UseFetchSloListResponse { const { - http, notifications: { toasts }, } = useKibana().services; + const { sloClient } = usePluginContext(); + const queryClient = useQueryClient(); const { dataView } = useCreateDataView({ @@ -95,15 +96,17 @@ export function useFetchSloList({ lastRefresh, }), queryFn: async ({ signal }) => { - return await http.get(`/api/observability/slos`, { - query: { - ...(kqlQuery && { kqlQuery }), - ...(sortBy && { sortBy }), - ...(sortDirection && { sortDirection }), - ...(page !== undefined && { page }), - ...(perPage !== undefined && { perPage }), - ...(filters && { filters }), - hideStale: true, + return await sloClient.fetch('GET /api/observability/slos 2023-10-31', { + params: { + query: { + ...(kqlQuery && { kqlQuery }), + ...(sortBy && { sortBy }), + ...(sortDirection && { sortDirection }), + ...(page !== undefined && { page: String(page) }), + ...(perPage !== undefined && { perPage: String(perPage) }), + ...(filters && { filters }), + hideStale: true, + }, }, signal, }); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slos_with_burn_rate_rules.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slos_with_burn_rate_rules.ts index 34cfa36bfcab8..ce1efab910723 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slos_with_burn_rate_rules.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slos_with_burn_rate_rules.ts @@ -14,7 +14,7 @@ import { import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import { BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common'; import { HttpSetup } from '@kbn/core/public'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; import { WindowSchema } from '../typings'; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_synthetics_suggestions.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_synthetics_suggestions.ts index f985b1f53ca94..f97bf64f5c8da 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_synthetics_suggestions.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_synthetics_suggestions.ts @@ -7,7 +7,7 @@ import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; export interface Suggestion { label: string; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts index f8e65fea9edff..5b4dc32ca4f28 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_get_preview_data.ts @@ -7,8 +7,8 @@ import { GetPreviewDataResponse, Indicator, Objective } from '@kbn/slo-schema'; import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; export interface UseGetPreviewData { data: GetPreviewDataResponse | undefined; @@ -18,6 +18,20 @@ export interface UseGetPreviewData { isError: boolean; } +interface Props { + isValid: boolean; + groupBy?: string | string[]; + instanceId?: string; + remoteName?: string; + groupings?: Record; + objective?: Objective; + indicator: Indicator; + range: { + from: Date; + to: Date; + }; +} + export function useGetPreviewData({ isValid, range, @@ -27,36 +41,29 @@ export function useGetPreviewData({ groupings, instanceId, remoteName, -}: { - isValid: boolean; - groupBy?: string | string[]; - instanceId?: string; - remoteName?: string; - groupings?: Record; - objective?: Objective; - indicator: Indicator; - range: { from: Date; to: Date }; -}): UseGetPreviewData { - const { http } = useKibana().services; +}: Props): UseGetPreviewData { + const { sloClient } = usePluginContext(); const { isInitialLoading, isLoading, isError, isSuccess, data } = useQuery({ queryKey: sloKeys.preview(indicator, range, groupings), queryFn: async ({ signal }) => { - const response = await http.post( - '/internal/observability/slos/_preview', - { - body: JSON.stringify({ + const response = await sloClient.fetch('POST /internal/observability/slos/_preview', { + params: { + body: { indicator, - range, + range: { + from: range.from.toISOString(), + to: range.to.toISOString(), + }, groupBy, instanceId, groupings, remoteName, ...(objective ? { objective } : null), - }), - signal, - } - ); + }, + }, + signal, + }); return Array.isArray(response) ? response : []; }, diff --git a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_kibana.ts similarity index 80% rename from x-pack/plugins/observability_solution/slo/public/utils/kibana_react.ts rename to x-pack/plugins/observability_solution/slo/public/hooks/use_kibana.ts index 20ec03497b0af..5880a52a11ee6 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_kibana.ts @@ -8,14 +8,15 @@ import { CoreStart } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { SloPublicPluginsStart } from '../types'; +import { SLOPublicPluginsStart } from '../types'; -export type StartServices = CoreStart & - SloPublicPluginsStart & +type StartServices = CoreStart & + SLOPublicPluginsStart & AdditionalServices & { storage: Storage; kibanaVersion: string; }; + const useTypedKibana = () => useKibana>(); diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_license.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_license.ts index 823c9e4135bf7..763e0bb1b0228 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_license.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_license.ts @@ -9,7 +9,7 @@ import { useCallback } from 'react'; import { Observable } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; interface UseLicenseReturnValue { getLicense: () => ILicense | null; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.test.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.test.ts index c9646c070dbbb..7d70e5f6460ff 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.test.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.test.ts @@ -5,11 +5,11 @@ * 2.0. */ import { sloFeatureId } from '@kbn/observability-shared-plugin/common'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { useFetchSloGlobalDiagnosis } from './use_fetch_global_diagnosis'; import { usePermissions } from './use_permissions'; -jest.mock('../utils/kibana_react'); +jest.mock('./use_kibana'); jest.mock('./use_fetch_global_diagnosis'); const useKibanaMock = useKibana as jest.Mock; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.ts index 6e380fbe1a33e..efd6c6cab94c9 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_permissions.ts @@ -5,7 +5,7 @@ * 2.0. */ import { sloFeatureId } from '@kbn/observability-plugin/common'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; import { useFetchSloGlobalDiagnosis } from './use_fetch_global_diagnosis'; export function usePermissions() { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts index 18d09d5859145..fdca20517102e 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts @@ -7,23 +7,27 @@ import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; +import { ResetSLOResponse } from '@kbn/slo-schema'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; type ServerError = IHttpFetchError; export function useResetSlo() { const { - http, notifications: { toasts }, } = useKibana().services; const queryClient = useQueryClient(); + const { sloClient } = usePluginContext(); - return useMutation( + return useMutation( ['resetSlo'], ({ id, name }) => { try { - return http.post(`/api/observability/slos/${id}/_reset`); + return sloClient.fetch('POST /api/observability/slos/{id}/_reset 2023-10-31', { + params: { path: { id } }, + }); } catch (error) { return Promise.reject( i18n.translate('xpack.slo.slo.reset.errorMessage', { diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts index c52056f006de2..8e348c1772398 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_space.ts @@ -6,7 +6,7 @@ */ import { useState, useEffect } from 'react'; -import { useKibana } from '../utils/kibana_react'; +import { useKibana } from './use_kibana'; export function useSpace() { const { spaces } = useKibana().services; diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_update_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_update_slo.ts index 21e9aa57c142d..2050b9cc101a7 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_update_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_update_slo.ts @@ -10,9 +10,10 @@ import { i18n } from '@kbn/i18n'; import { encode } from '@kbn/rison'; import type { FindSLOResponse, UpdateSLOInput, UpdateSLOResponse } from '@kbn/slo-schema'; import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; import { paths } from '../../common/locators/paths'; +import { useKibana } from './use_kibana'; import { sloKeys } from './query_key_factory'; +import { usePluginContext } from './use_plugin_context'; type ServerError = IHttpFetchError; @@ -23,6 +24,7 @@ export function useUpdateSlo() { notifications: { toasts }, } = useKibana().services; const queryClient = useQueryClient(); + const { sloClient } = usePluginContext(); return useMutation< UpdateSLOResponse, @@ -32,8 +34,9 @@ export function useUpdateSlo() { >( ['updateSlo'], ({ sloId, slo }) => { - const body = JSON.stringify(slo); - return http.put(`/api/observability/slos/${sloId}`, { body }); + return sloClient.fetch('PUT /api/observability/slos/{id} 2023-10-31', { + params: { path: { id: sloId }, body: slo }, + }); }, { onSuccess: (_data, { slo: { name } }) => { diff --git a/x-pack/plugins/observability_solution/slo/public/index.ts b/x-pack/plugins/observability_solution/slo/public/index.ts index e57edb2205208..7d01e9c418fb5 100644 --- a/x-pack/plugins/observability_solution/slo/public/index.ts +++ b/x-pack/plugins/observability_solution/slo/public/index.ts @@ -5,20 +5,26 @@ * 2.0. */ import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; -import { SloPlugin } from './plugin'; +import { SLOPlugin } from './plugin'; import type { - SloPublicSetup, - SloPublicStart, - SloPublicPluginsSetup, - SloPublicPluginsStart, + SLOPublicSetup, + SLOPublicStart, + SLOPublicPluginsSetup, + SLOPublicPluginsStart, } from './types'; export const plugin: PluginInitializer< - SloPublicSetup, - SloPublicStart, - SloPublicPluginsSetup, - SloPublicPluginsStart + SLOPublicSetup, + SLOPublicStart, + SLOPublicPluginsSetup, + SLOPublicPluginsStart > = (initializerContext: PluginInitializerContext) => { - return new SloPlugin(initializerContext); + return new SLOPlugin(initializerContext); }; -export type { SloPublicPluginsSetup, SloPublicPluginsStart, SloPublicStart } from './types'; + +export type { + SLOPublicPluginsSetup, + SLOPublicPluginsStart, + SLOPublicStart, + SLOPublicSetup, +} from './types'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx index 8b15c809ab1d0..4f6b5a159c62a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx @@ -12,7 +12,7 @@ import numeral from '@elastic/numeral'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { TimeBounds } from '../types'; import { SloTabId } from './slo_details'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { toDuration, toMinutes } from '../../../utils/slo/duration'; import { ChartData } from '../../../typings/slo'; import { WideChart } from './wide_chart'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx index b8ffb2f30a79e..72c6cf30c9de6 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx @@ -16,7 +16,7 @@ import React, { useState, useCallback } from 'react'; import { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public'; import { TimeBounds } from '../types'; import { SloTabId } from './slo_details'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { ChartData } from '../../../typings/slo'; import { ErrorBudgetChart } from './error_budget_chart'; import { ErrorBudgetHeader } from './error_budget_header'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.test.tsx index 65a4033314972..533fd6b499a7a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { render } from '../../../utils/test_helper'; import { buildSlo } from '../../../data/slo/slo'; import { ErrorBudgetHeader } from './error_budget_header'; -jest.mock('../../../utils/kibana_react'); +jest.mock('../../../hooks/use_kibana'); const useKibanaMock = useKibana as jest.Mock; describe('In Observability Context', () => { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx index 4a484d9df013f..11d1b1e600b0d 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { SloTabId } from './slo_details'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels'; import { ErrorBudgetActions } from './error_budget_actions'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_area_chart.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_area_chart.tsx index 01496c6471d89..364e775eaf0e4 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_area_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_area_chart.tsx @@ -16,7 +16,7 @@ import { useActiveCursor } from '@kbn/charts-plugin/public'; import moment from 'moment'; import { getBrushTimeBounds } from '../../../utils/slo/duration'; import { TimeBounds } from '../types'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; export function EventsAreaChart({ slo, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx index 4b393e307deb3..3ea9cb30627ad 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx @@ -25,7 +25,7 @@ import { EventsAreaChart } from './events_area_chart'; import { TimeBounds } from '../types'; import { SloTabId } from './slo_details'; import { useGetPreviewData } from '../../../hooks/use_get_preview_data'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { GoodBadEventsChart } from '../../../components/good_bad_events_chart/good_bad_events_chart'; import { getDiscoverLink } from '../../../utils/slo/get_discover_link'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx index fb1e524e3e170..e4ccb4f0cbf26 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx @@ -24,7 +24,7 @@ import { useCloneSlo } from '../../../hooks/use_clone_slo'; import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo'; import { usePermissions } from '../../../hooks/use_permissions'; import { useResetSlo } from '../../../hooks/use_reset_slo'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { isApmIndicatorType } from '../../../utils/slo/indicator'; import { EditBurnRateRuleFlyout } from '../../slos/components/common/edit_burn_rate_rule_flyout'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/history/slo_details_history.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/history/slo_details_history.tsx index 7ecf4d4672775..45177fa0d73fe 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/history/slo_details_history.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/history/slo_details_history.tsx @@ -16,7 +16,7 @@ import DateMath from '@kbn/datemath'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import React, { useMemo, useState } from 'react'; import { BurnRates } from '../../../../components/slo/burn_rate/burn_rates'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { useBurnRateOptions } from '../../hooks/use_burn_rate_options'; import { TimeBounds } from '../../types'; import { EventsChartPanel } from '../events_chart_panel'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/apm_indicator_overview.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/apm_indicator_overview.tsx index 6520c061d6d07..be1cc2b15fd8a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/apm_indicator_overview.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/apm_indicator_overview.tsx @@ -14,7 +14,7 @@ import { SLOWithSummaryResponse, } from '@kbn/slo-schema'; import React from 'react'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { OverviewItem } from './overview_item'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/display_query.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/display_query.tsx index 0d6674cc4036c..db85ad817f464 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/display_query.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/display_query.tsx @@ -10,7 +10,7 @@ import { QuerySchema } from '@kbn/slo-schema'; import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FilterItem } from '@kbn/unified-search-plugin/public'; import { injectI18n } from '@kbn/i18n-react'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { useCreateDataView } from '../../../../hooks/use_create_data_view'; const FilterItemI18n = injectI18n(FilterItem); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/overview.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/overview.tsx index c974c91b00a69..34f3b0132dc8a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/overview.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/overview.tsx @@ -17,7 +17,7 @@ import { import React from 'react'; import { TagsList } from '@kbn/observability-shared-plugin/public'; import { DisplayQuery } from './display_query'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { BUDGETING_METHOD_OCCURRENCES, BUDGETING_METHOD_TIMESLICES, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/synthetics_indicator_overview.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/synthetics_indicator_overview.tsx index a475428a6005b..6083d988a8b98 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/synthetics_indicator_overview.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/overview/synthetics_indicator_overview.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { syntheticsAvailabilityIndicatorSchema, SLOWithSummaryResponse } from '@kbn/slo-schema'; import React from 'react'; import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { OverviewItem } from './overview_item'; interface Props { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx index 9a7c814630aae..0225c380737a4 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx @@ -12,7 +12,7 @@ import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-sc import React from 'react'; import { TimeBounds } from '../types'; import { SloTabId } from './slo_details'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { ChartData } from '../../../typings/slo'; import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels'; import { WideChart } from './wide_chart'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx index 7e002d1cac7d1..3aa94c00b6441 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx @@ -10,7 +10,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { SLO_ALERTS_TABLE_ID } from '@kbn/observability-shared-plugin/common'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; export interface Props { slo: SLOWithSummaryResponse; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_health_callout.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_health_callout.tsx index 0b9e312910319..ab34e2428786c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_health_callout.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_health_callout.tsx @@ -19,7 +19,7 @@ import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import React from 'react'; import { getSLOSummaryTransformId, getSLOTransformId } from '../../../../common/constants'; import { useFetchSloHealth } from '../../../hooks/use_fetch_slo_health'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; export function SloHealthCallout({ slo }: { slo: SLOWithSummaryResponse }) { const { http } = useKibana().services; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx index faff275ffbeac..b1354205990af 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx @@ -26,7 +26,7 @@ import { useAnnotations } from '@kbn/observability-plugin/public'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { getBrushTimeBounds } from '../../../utils/slo/duration'; import { TimeBounds } from '../types'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { ChartData } from '../../../typings'; type ChartType = 'area' | 'line'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts index 44a6b8979ecd6..8e7be877ca02e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts @@ -12,7 +12,7 @@ import path from 'path'; import { paths } from '../../../../common/locators/paths'; import { useSpace } from '../../../hooks/use_space'; import { BurnRateRuleParams } from '../../../typings'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { createRemoteSloDeleteUrl, createRemoteSloEditUrl, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx index 8eb954d4b5771..83acc81a68716 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx @@ -10,7 +10,7 @@ import { EuiNotificationBadge, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { paths } from '../../../../common/locators/paths'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { useFetchActiveAlerts } from '../../../hooks/use_fetch_active_alerts'; import { ALERTS_TAB_ID, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx index eb678b8ca7418..a363fdccb16f8 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx @@ -30,7 +30,7 @@ import { useFetchHistoricalSummary } from '../../hooks/use_fetch_historical_summ import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details'; import { useLicense } from '../../hooks/use_license'; import { usePermissions } from '../../hooks/use_permissions'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { render } from '../../utils/test_helper'; import { SloDetailsPage } from './slo_details'; import { usePerformanceContext } from '@kbn/ebt-tools'; @@ -41,7 +41,7 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('@kbn/observability-shared-plugin/public'); -jest.mock('../../utils/kibana_react'); +jest.mock('../../hooks/use_kibana'); jest.mock('../../hooks/use_license'); jest.mock('../../hooks/use_permissions'); jest.mock('../../hooks/use_fetch_active_alerts'); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx index 9a32c150e1b8c..38f65bb341070 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx @@ -24,7 +24,7 @@ import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details'; import { useLicense } from '../../hooks/use_license'; import { usePermissions } from '../../hooks/use_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import PageNotFound from '../404'; import { HeaderControl } from './components/header_control'; import { HeaderTitle } from './components/header_title'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/data_preview_chart.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/data_preview_chart.tsx index e797c4d5d3b3d..60d464b1f0503 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/data_preview_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/data_preview_chart.tsx @@ -37,7 +37,7 @@ import { max, min } from 'lodash'; import moment from 'moment'; import React, { useState } from 'react'; import { useFormContext } from 'react-hook-form'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { GoodBadEventsChart } from '../../../../components/good_bad_events_chart/good_bad_events_chart'; import { useDebouncedGetPreviewData } from '../../hooks/use_preview'; import { useSectionFormValidation } from '../../hooks/use_section_form_validation'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/documents_table.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/documents_table.tsx index ed250da853f9e..546b443aa23c7 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/documents_table.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/documents_table.tsx @@ -16,7 +16,7 @@ import { EuiResizableContainer, EuiProgress, EuiCallOut, EuiSpacer } from '@elas import { buildFilter, FILTERS, TimeRange } from '@kbn/es-query'; import { FieldPath, useFormContext } from 'react-hook-form'; import { Serializable } from '@kbn/utility-types'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { CreateSLOForm } from '../../types'; import { QuerySearchBar } from './query_search_bar'; import { SearchBarProps } from './query_builder'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx index 394d8c303e953..e7e61adfc1cc5 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/query_search_bar.tsx @@ -13,7 +13,7 @@ import React, { memo } from 'react'; import styled from 'styled-components'; import { observabilityAppId } from '@kbn/observability-shared-plugin/common'; import { SearchBarProps } from './query_builder'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { CreateSLOForm } from '../../types'; import { OptionalText } from './optional_text'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/req_code_viewer.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/req_code_viewer.tsx index f160dc0aa33c0..451997319a03d 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/req_code_viewer.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/req_code_viewer.tsx @@ -13,7 +13,7 @@ import { XJsonLang } from '@kbn/monaco'; import React, { ReactNode, useCallback } from 'react'; import { CodeEditor } from '@kbn/code-editor'; import { compressToEncodedURIComponent } from 'lz-string'; -import { useKibana } from '../../../../../utils/kibana_react'; +import { useKibana } from '../../../../../hooks/use_kibana'; interface RequestCodeViewerProps { value: string; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/slo_inspect.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/slo_inspect.tsx index 18d0d966c1ac0..1b4a49bc81894 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/slo_inspect.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/slo_inspect/slo_inspect.tsx @@ -27,7 +27,7 @@ import { GetSLOResponse } from '@kbn/slo-schema'; import React, { useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { enableInspectEsQueries } from '@kbn/observability-plugin/common'; -import { useKibana } from '../../../../../utils/kibana_react'; +import { useKibana } from '../../../../../hooks/use_kibana'; import { useFetchSloInspect } from '../../../../../hooks/use_fetch_slo_inspect'; import { usePluginContext } from '../../../../../hooks/use_plugin_context'; import { transformCreateSLOFormToCreateSLOInput } from '../../../helpers/process_slo_form_values'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/use_field_sidebar.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/use_field_sidebar.tsx index 71d6ab00a77af..37467eb33c272 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/use_field_sidebar.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/common/use_field_sidebar.tsx @@ -8,7 +8,7 @@ import React, { useMemo, SetStateAction } from 'react'; import { UnifiedFieldListSidebarContainer } from '@kbn/unified-field-list'; import { DataView } from '@kbn/data-views-plugin/common'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; export const useFieldSidebar = ({ dataView, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx index 7332c94103c7e..146d11be84ac8 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/index_selection.tsx @@ -12,8 +12,8 @@ import React, { useEffect } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { DataViewPicker } from '@kbn/unified-search-plugin/public'; import { getDataViewPattern, useAdhocDataViews } from './use_adhoc_data_views'; -import { SloPublicPluginsStart } from '../../../..'; -import { useKibana } from '../../../../utils/kibana_react'; +import { SLOPublicPluginsStart } from '../../../..'; +import { useKibana } from '../../../../hooks/use_kibana'; import { CreateSLOForm } from '../../types'; const BTN_MAX_WIDTH = 515; @@ -26,7 +26,7 @@ export function IndexSelection({ selectedDataView }: { selectedDataView?: DataVi const { control, getFieldState, setValue, watch } = useFormContext(); const { dataViews: dataViewsService, dataViewFieldEditor } = useKibana().services; - const { dataViewEditor } = useKibana().services; + const { dataViewEditor } = useKibana().services; const currentIndexPattern = watch(INDEX_FIELD); const currentDataViewId = watch(DATA_VIEW_FIELD); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts index 6c8288df6b9d9..67792b056408d 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/custom_common/use_adhoc_data_views.ts @@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'; import { DataView, DataViewListItem } from '@kbn/data-views-plugin/common'; import { useFetchDataViews } from '@kbn/observability-plugin/public'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; export const getDataViewPattern = ({ byId, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx index 29150231046d1..ab0c7ff235fbf 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/slo_edit_form_footer.tsx @@ -12,7 +12,7 @@ import React, { useCallback, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; import { InPortal } from 'react-reverse-portal'; import { useCreateRule } from '../../../hooks/use_create_burn_rate_rule'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { sloEditFormFooterPortal } from '../shared_flyout/slo_add_form_flyout'; import { paths } from '../../../../common/locators/paths'; import { useCreateSlo } from '../../../hooks/use_create_slo'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_common/field_selector.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_common/field_selector.tsx index 5bc77b186f960..1fea21a322c72 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_common/field_selector.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/synthetics_common/field_selector.tsx @@ -120,7 +120,6 @@ export function FieldSelector({ ? (field.value as Array<{ value: string; label: string }>).map((value) => ({ value: value.value, label: value.label, - 'data-test-subj': `${dataTestSubj}SelectedValue`, })) : [] } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx index 548f0bd0ab3e5..86eede0ba65e2 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/components/timeslice_metric/timeslice_metric_indicator.tsx @@ -19,7 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import { IndexAndTimestampField } from '../custom_common/index_and_timestamp_field'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { GroupByField } from '../common/group_by_field'; import { CreateSLOForm } from '../../types'; import { DataPreviewChart } from '../common/data_preview_chart'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_fetch_suggestions.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_fetch_suggestions.ts index ff9ebffa72f56..4cfe886b17b69 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_fetch_suggestions.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/hooks/use_fetch_suggestions.ts @@ -6,22 +6,18 @@ */ import { useQuery } from '@tanstack/react-query'; -import { GetSLOSuggestionsResponse } from '@kbn/slo-schema'; -import { useKibana } from '../../../utils/kibana_react'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; export function useFetchSLOSuggestions() { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const { isLoading, isError, isSuccess, data } = useQuery({ queryKey: ['fetchSLOSuggestions'], queryFn: async ({ signal }) => { try { - return await http.get( - '/internal/observability/slos/suggestions', - { - signal, - } - ); + return await sloClient.fetch('GET /internal/observability/slos/suggestions', { + signal, + }); } catch (error) { // ignore error } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/slo_add_form_flyout.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/slo_add_form_flyout.tsx index c3b51a0058fed..98c76b190aa1a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/slo_add_form_flyout.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/slo_add_form_flyout.tsx @@ -4,19 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; -import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import { RecursivePartial } from '@kbn/utility-types'; import { merge } from 'lodash'; -import { createHtmlPortalNode, OutPortal } from 'react-reverse-portal'; -import { CreateSLOForm } from '../types'; +import React from 'react'; +import { OutPortal, createHtmlPortalNode } from 'react-reverse-portal'; import { SloEditForm } from '../components/slo_edit_form'; +import { CreateSLOForm } from '../types'; export const sloEditFormFooterPortal = createHtmlPortalNode(); -function SloAddFormFlyout({ +// eslint-disable-next-line import/no-default-export +export default function SloAddFormFlyout({ onClose, initialValues, }: { @@ -69,6 +70,3 @@ function SloAddFormFlyout({ ); } - -// eslint-disable-next-line import/no-default-export -export default SloAddFormFlyout; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/slo_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/slo_form.tsx deleted file mode 100644 index 8c105e6159797..0000000000000 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/slo_form.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { lazy } from 'react'; -import React, { Suspense } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiLoadingSpinnerProps } from '@elastic/eui'; - -function CenterJustifiedSpinner({ size }: { size: EuiLoadingSpinnerProps['size'] }) { - return ( - - - - - - ); -} - -function suspendedComponentWithProps( - ComponentToSuspend: React.ComponentType, - size?: EuiLoadingSpinnerProps['size'] -) { - return (props: T) => ( - }> - {/* @ts-expect-error upgrade typescript v4.9.5 */} - - - ); -} - -export const SloAddFormFlyout = suspendedComponentWithProps( - lazy(() => import('./slo_add_form_flyout')) -); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx index f5acb4e964f08..abc60d6a00352 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.test.tsx @@ -25,7 +25,7 @@ import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details'; import { usePermissions } from '../../hooks/use_permissions'; import { useCreateRule } from '../../hooks/use_create_burn_rate_rule'; import { useUpdateSlo } from '../../hooks/use_update_slo'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { kibanaStartMock } from '../../utils/kibana_react.mock'; import { render } from '../../utils/test_helper'; import { SLO_EDIT_FORM_DEFAULT_VALUES } from './constants'; @@ -49,7 +49,7 @@ jest.mock('../../hooks/use_create_burn_rate_rule'); const mockUseKibanaReturnValue = kibanaStartMock.startContract(); -jest.mock('../../utils/kibana_react', () => ({ +jest.mock('../../hooks/use_kibana', () => ({ useKibana: jest.fn(() => mockUseKibanaReturnValue), })); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx index 7dcce93c0d003..b014bdb1d6dec 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/slo_edit.tsx @@ -15,7 +15,7 @@ import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details'; import { useLicense } from '../../hooks/use_license'; import { usePermissions } from '../../hooks/use_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { SloEditForm } from './components/slo_edit_form'; export function SloEditPage() { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx index 5a35061b464e5..edd26e579b2a1 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/index.tsx @@ -15,7 +15,7 @@ import { useFetchSloDefinitions } from '../../hooks/use_fetch_slo_definitions'; import { useLicense } from '../../hooks/use_license'; import { usePermissions } from '../../hooks/use_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { SloListEmpty } from '../slos/components/slo_list_empty'; import { OutdatedSlo } from './outdated_slo'; import { OutdatedSloSearchBar } from './outdated_slo_search_bar'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_get_settings.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/hooks/use_get_settings.ts similarity index 75% rename from x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_get_settings.ts rename to x-pack/plugins/observability_solution/slo/public/pages/slo_settings/hooks/use_get_settings.ts index 88d38bc7f936d..eb8d9642790cd 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_get_settings.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/hooks/use_get_settings.ts @@ -7,16 +7,16 @@ import { GetSLOSettingsResponse } from '@kbn/slo-schema'; import { useQuery } from '@tanstack/react-query'; -import { DEFAULT_STALE_SLO_THRESHOLD_HOURS } from '../../../common/constants'; -import { useKibana } from '../../utils/kibana_react'; +import { DEFAULT_STALE_SLO_THRESHOLD_HOURS } from '../../../../common/constants'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; export const useGetSettings = () => { - const { http } = useKibana().services; + const { sloClient } = usePluginContext(); const { isLoading, data } = useQuery({ queryKey: ['getSloSettings'], queryFn: async ({ signal }) => { try { - return http.get('/internal/slo/settings', { signal }); + return await sloClient.fetch('GET /internal/slo/settings', { signal }); } catch (error) { return defaultSettings; } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_put_slo_settings.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/hooks/use_put_slo_settings.tsx similarity index 81% rename from x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_put_slo_settings.tsx rename to x-pack/plugins/observability_solution/slo/public/pages/slo_settings/hooks/use_put_slo_settings.tsx index 48c9a54eea295..e62204d0abb6d 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/use_put_slo_settings.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/hooks/use_put_slo_settings.tsx @@ -9,8 +9,9 @@ import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { PutSLOSettingsParams, PutSLOSettingsResponse } from '@kbn/slo-schema'; import { useMutation } from '@tanstack/react-query'; -import { paths } from '../../../common/locators/paths'; -import { useKibana } from '../../utils/kibana_react'; +import { paths } from '../../../../common/locators/paths'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; +import { useKibana } from '../../../hooks/use_kibana'; type ServerError = IHttpFetchError; @@ -20,12 +21,14 @@ export function usePutSloSettings() { http, notifications: { toasts }, } = useKibana().services; + const { sloClient } = usePluginContext(); return useMutation( ['putSloSettings'], ({ settings }) => { - const body = JSON.stringify(settings); - return http.put(`/internal/slo/settings`, { body }); + return sloClient.fetch(`PUT /internal/slo/settings`, { + params: { body: settings }, + }); }, { onSuccess: (data, { settings }) => { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx index 895ca2c0f8e2b..c6910bf53810d 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/settings_form.tsx @@ -7,25 +7,25 @@ import { i18n } from '@kbn/i18n'; import { - EuiForm, - EuiFormRow, - EuiSwitch, - EuiDescribedFormGroup, + EuiButton, + EuiButtonEmpty, EuiComboBox, + EuiDescribedFormGroup, + EuiFieldNumber, EuiFlexGroup, EuiFlexItem, - EuiButtonEmpty, - EuiButton, + EuiForm, + EuiFormRow, EuiSpacer, - EuiFieldNumber, + EuiSwitch, } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; -import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { isEqual } from 'lodash'; +import React, { useEffect, useState } from 'react'; import { DEFAULT_STALE_SLO_THRESHOLD_HOURS } from '../../../common/constants'; -import { useGetSettings } from './use_get_settings'; -import { usePutSloSettings } from './use_put_slo_settings'; +import { useGetSettings } from './hooks/use_get_settings'; +import { usePutSloSettings } from './hooks/use_put_slo_settings'; export function SettingsForm() { const [useAllRemoteClusters, setUseAllRemoteClusters] = useState(false); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx index ca41c7561fb46..5c3ae879058ce 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_settings/slo_settings.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import { SettingsForm } from './settings_form'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { paths } from '../../../common/locators/paths'; import { HeaderMenu } from '../../components/header_menu/header_menu'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx index a721499e9ef01..e8368f96a6d34 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/badges/slo_indicator_type_badge.tsx @@ -17,7 +17,7 @@ import { euiLightVars } from '@kbn/ui-theme'; import React, { MouseEvent } from 'react'; import { useRouteMatch } from 'react-router-dom'; import { SLOS_PATH } from '../../../../../common/locators/paths'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { toIndicatorTypeLabel } from '../../../../utils/slo/labels'; import { useUrlSearchState } from '../../hooks/use_url_search_state'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx index fe274cdabd6a7..7800337be79ae 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx @@ -22,7 +22,7 @@ import { SloDeleteModal } from '../../../../components/slo/delete_confirmation_m import { SloResetConfirmationModal } from '../../../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal'; import { useResetSlo } from '../../../../hooks/use_reset_slo'; import { BurnRateRuleParams } from '../../../../typings'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter'; import { useSloListActions } from '../../hooks/use_slo_list_actions'; import { useSloFormattedSummary } from '../../hooks/use_slo_summary'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx index 609e4d02cd19a..f8e6890483404 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx @@ -11,7 +11,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { sloFeatureId } from '@kbn/observability-plugin/common'; import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule_types'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { paths } from '../../../../../common/locators/paths'; import { sloKeys } from '../../../../hooks/query_key_factory'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/create_slo_btn.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/create_slo_btn.tsx index 7add8debe165b..843ae5f268f49 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/create_slo_btn.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/create_slo_btn.tsx @@ -8,7 +8,7 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { paths } from '../../../../../common/locators/paths'; import { usePermissions } from '../../../../hooks/use_permissions'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx index 1a8577e4a83fe..89701b0922f87 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { BurnRateRuleParams } from '../../../../typings'; export function EditBurnRateRuleFlyout({ diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx index 0d556c0546e1f..03fbb0174f303 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx @@ -36,7 +36,7 @@ import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule import { usePermissions } from '../../../../hooks/use_permissions'; import { useResetSlo } from '../../../../hooks/use_reset_slo'; import { useSpace } from '../../../../hooks/use_space'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter'; import { createRemoteSloDeleteUrl, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx index 8342c9aa5976b..a433b57073e3b 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx @@ -26,7 +26,7 @@ import { GroupSummary } from '@kbn/slo-schema'; import React, { memo, useState } from 'react'; import { paths } from '../../../../../common/locators/paths'; import { useFetchSloList } from '../../../../hooks/use_fetch_slo_list'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { useSloFormattedSLIValue } from '../../hooks/use_slo_summary'; import type { SortDirection, SortField } from '../../hooks/use_url_search_state'; import { SlosView } from '../slos_view'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/health_callout/health_callout.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/health_callout/health_callout.tsx index ce956b00a8170..c4befdfe6b92f 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/health_callout/health_callout.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/health_callout/health_callout.tsx @@ -20,7 +20,7 @@ import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import React, { useState } from 'react'; import { getSLOSummaryTransformId, getSLOTransformId } from '../../../../../common/constants'; import { useFetchSloHealth } from '../../../../hooks/use_fetch_slo_health'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; const CALLOUT_SESSION_STORAGE_KEY = 'slo_health_callout_hidden'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx index 1332a78c1e484..1a99f8ff354d9 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx @@ -22,7 +22,7 @@ import styled from 'styled-components'; import { usePermissions } from '../../../hooks/use_permissions'; import { useCloneSlo } from '../../../hooks/use_clone_slo'; import { BurnRateRuleParams } from '../../../typings'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { useSloActions } from '../../slo_details/hooks/use_slo_actions'; interface Props { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list.tsx index 0fad1e96975ea..f0fec063ad23e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list.tsx @@ -13,7 +13,7 @@ import dedent from 'dedent'; import { groupBy as _groupBy, mapValues } from 'lodash'; import React, { useEffect } from 'react'; import { useFetchSloList } from '../../../hooks/use_fetch_slo_list'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { useUrlSearchState } from '../hooks/use_url_search_state'; import { GroupView } from './grouped_slos/group_view'; import { ToggleSLOView } from './toggle_slo_view'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx index d7175553fd178..7223f91d49519 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiSelectableOption, EuiText } from '@elastic/eui'; import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { useGetSettings } from '../../slo_settings/use_get_settings'; +import { useGetSettings } from '../../slo_settings/hooks/use_get_settings'; import type { SearchState } from '../hooks/use_url_search_state'; import type { Option } from './slo_context_menu'; import { ContextMenuItem, SLOContextMenu } from './slo_context_menu'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx index 69cc895121d01..16c635a2da20c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_search_bar.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { observabilityAppId } from '@kbn/observability-plugin/public'; import React, { useEffect } from 'react'; import styled from 'styled-components'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { useSloCrudLoading } from '../hooks/use_crud_loading'; import { useSloSummaryDataView } from '../hooks/use_summary_dataview'; import { useUrlSearchState } from '../hooks/use_url_search_state'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx index d00152e425dfd..1315e2b6c829a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_sparkline.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiLoadingChart, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; interface Data { key: number; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx index 808c9096a3793..9fba8b59bef4a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slo_overview_alerts.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { GetOverviewResponse } from '@kbn/slo-schema/src/rest_specs/routes/get_overview'; import { rulesLocatorID, RulesParams } from '@kbn/observability-plugin/public'; import { useAlertsUrl } from '../../../../hooks/use_alerts_url'; -import { useKibana } from '../../../../utils/kibana_react'; +import { useKibana } from '../../../../hooks/use_kibana'; import { OverViewItem } from './overview_item'; export function SLOOverviewAlerts({ diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx index ab551637a6f25..42c0d199db788 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slos_overview/slos_overview.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { DEFAULT_STALE_SLO_THRESHOLD_HOURS } from '../../../../../common/constants'; import { SLOOverviewAlerts } from './slo_overview_alerts'; -import { useGetSettings } from '../../../slo_settings/use_get_settings'; +import { useGetSettings } from '../../../slo_settings/hooks/use_get_settings'; import { useFetchSLOsOverview } from '../../hooks/use_fetch_slos_overview'; import { useUrlSearchState } from '../../hooks/use_url_search_state'; import { OverViewItem } from './overview_item'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_fetch_slos_overview.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_fetch_slos_overview.ts index 783c23d49a42b..1e9246d97f795 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_fetch_slos_overview.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_fetch_slos_overview.ts @@ -4,15 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useQuery } from '@tanstack/react-query'; -import { i18n } from '@kbn/i18n'; import { buildQueryFromFilters, Filter } from '@kbn/es-query'; -import { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { GetOverviewResponse } from '@kbn/slo-schema/src/rest_specs/routes/get_overview'; -import { sloKeys } from '../../../hooks/query_key_factory'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../../common/constants'; +import { sloKeys } from '../../../hooks/query_key_factory'; import { useCreateDataView } from '../../../hooks/use_create_data_view'; -import { useKibana } from '../../../utils/kibana_react'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; +import { useKibana } from '../../../hooks/use_kibana'; import { SearchState } from './use_url_search_state'; interface SLOsOverviewParams { @@ -38,8 +39,8 @@ export function useFetchSLOsOverview({ filters: filterDSL = [], lastRefresh, }: SLOsOverviewParams = {}): UseFetchSloGroupsResponse { + const { sloClient } = usePluginContext(); const { - http, notifications: { toasts }, } = useKibana().services; @@ -73,10 +74,12 @@ export function useFetchSLOsOverview({ lastRefresh, }), queryFn: async ({ signal }) => { - return await http.get('/internal/observability/slos/overview', { - query: { - ...(kqlQuery && { kqlQuery }), - ...(filters && { filters }), + return await sloClient.fetch('GET /internal/observability/slos/overview', { + params: { + query: { + ...(kqlQuery && { kqlQuery }), + ...(filters && { filters }), + }, }, signal, }); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts index 2d7d8b7d9a97f..d339c1e66bf00 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_list_actions.ts @@ -8,7 +8,7 @@ import { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useCallback } from 'react'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { SLO_OVERVIEW_EMBEDDABLE_ID } from '../../../embeddable/slo/overview/constants'; export function useSloListActions({ diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts index d2811774e59aa..0ad95a4408ab8 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_slo_summary.ts @@ -9,7 +9,7 @@ import numeral from '@elastic/numeral'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { IBasePath } from '@kbn/core-http-browser'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { useKibana } from '../../../utils/kibana_react'; +import { useKibana } from '../../../hooks/use_kibana'; import { paths } from '../../../../common/locators/paths'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts index 3d502cfe85ec7..ab5664ee49b43 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_summary_dataview.ts @@ -9,8 +9,8 @@ import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { useEffect, useState } from 'react'; import { getListOfSloSummaryIndices } from '../../../../common/summary_indices'; import { useCreateDataView } from '../../../hooks/use_create_data_view'; -import { useKibana } from '../../../utils/kibana_react'; -import { useGetSettings } from '../../slo_settings/use_get_settings'; +import { useKibana } from '../../../hooks/use_kibana'; +import { useGetSettings } from '../../slo_settings/hooks/use_get_settings'; export const useSloSummaryDataView = () => { const { http } = useKibana().services; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts index 59284ee617919..5da3b4596ed71 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/hooks/use_url_search_state.ts @@ -5,11 +5,11 @@ * 2.0. */ +import type { Filter } from '@kbn/es-query'; import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import deepmerge from 'deepmerge'; -import { useHistory } from 'react-router-dom'; -import { Filter } from '@kbn/es-query'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { DEFAULT_SLO_PAGE_SIZE } from '../../../../common/constants'; import type { GroupByField } from '../components/slo_list_group_by'; import type { SLOView } from '../components/toggle_slo_view'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx index 905e6088ef74b..3a9c21c2ffa0e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.test.tsx @@ -25,9 +25,9 @@ import { useFetchHistoricalSummary } from '../../hooks/use_fetch_historical_summ import { useFetchSloList } from '../../hooks/use_fetch_slo_list'; import { useLicense } from '../../hooks/use_license'; import { usePermissions } from '../../hooks/use_permissions'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { render } from '../../utils/test_helper'; -import { useGetSettings } from '../slo_settings/use_get_settings'; +import { useGetSettings } from '../slo_settings/hooks/use_get_settings'; import { SlosPage } from './slos'; jest.mock('react-router-dom', () => ({ @@ -36,11 +36,11 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('@kbn/observability-shared-plugin/public'); -jest.mock('../../utils/kibana_react'); +jest.mock('../../hooks/use_kibana'); jest.mock('../../hooks/use_license'); jest.mock('../../hooks/use_fetch_slo_list'); jest.mock('../../hooks/use_create_slo'); -jest.mock('../slo_settings/use_get_settings'); +jest.mock('../slo_settings/hooks/use_get_settings'); jest.mock('../../hooks/use_delete_slo'); jest.mock('../../hooks/use_delete_slo_instance'); jest.mock('../../hooks/use_fetch_historical_summary'); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.tsx index ba23f7af34c3d..ebc0dbb39c192 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/slos.tsx @@ -17,7 +17,7 @@ import { useFetchSloList } from '../../hooks/use_fetch_slo_list'; import { useLicense } from '../../hooks/use_license'; import { usePermissions } from '../../hooks/use_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { CreateSloBtn } from './components/common/create_slo_btn'; import { FeedbackButton } from './components/common/feedback_button'; import { SloList } from './components/slo_list'; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.test.tsx index 07676d1cbe4a3..369a1fbeed932 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.test.tsx @@ -14,7 +14,7 @@ import { emptySloList, sloList } from '../../data/slo/slo'; import { usePermissions } from '../../hooks/use_permissions'; import { useFetchSloList } from '../../hooks/use_fetch_slo_list'; import { useLicense } from '../../hooks/use_license'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import { render } from '../../utils/test_helper'; import { SlosWelcomePage } from './slos_welcome'; import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; @@ -25,7 +25,7 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('@kbn/observability-shared-plugin/public'); -jest.mock('../../utils/kibana_react'); +jest.mock('../../hooks/use_kibana'); jest.mock('../../hooks/use_license'); jest.mock('../../hooks/use_fetch_slo_list'); jest.mock('../../hooks/use_permissions'); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.tsx index fc03009beb72e..ccb273aad913e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos_welcome/slos_welcome.tsx @@ -25,7 +25,7 @@ import { useFetchSloList } from '../../hooks/use_fetch_slo_list'; import { useLicense } from '../../hooks/use_license'; import { usePermissions } from '../../hooks/use_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useKibana } from '../../utils/kibana_react'; +import { useKibana } from '../../hooks/use_kibana'; import illustration from './assets/illustration.svg'; export function SlosWelcomePage() { diff --git a/x-pack/plugins/observability_solution/slo/public/plugin.ts b/x-pack/plugins/observability_solution/slo/public/plugin.ts index 3e320238794bc..e61910e108a7d 100644 --- a/x-pack/plugins/observability_solution/slo/public/plugin.ts +++ b/x-pack/plugins/observability_solution/slo/public/plugin.ts @@ -14,10 +14,13 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/public'; +import { DefaultClientOptions, createRepositoryClient } from '@kbn/server-route-repository-client'; +import { lazy } from 'react'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { PLUGIN_NAME, sloAppId } from '../common'; -import { ExperimentalFeatures, SloConfig } from '../common/config'; +import { ExperimentalFeatures, SLOConfig } from '../common/config'; import { SLOS_BASE_PATH } from '../common/locators/paths'; +import type { SLORouteRepository } from '../server/routes/get_slo_server_route_repository'; import { SLO_ALERTS_EMBEDDABLE_ID } from './embeddable/slo/alerts/constants'; import { SLO_BURN_RATE_EMBEDDABLE_ID } from './embeddable/slo/burn_rate/constants'; import { SLO_ERROR_BUDGET_ID } from './embeddable/slo/error_budget/constants'; @@ -26,37 +29,41 @@ import { SloOverviewEmbeddableState } from './embeddable/slo/overview/types'; import { SloDetailsLocatorDefinition } from './locators/slo_details'; import { SloEditLocatorDefinition } from './locators/slo_edit'; import { SloListLocatorDefinition } from './locators/slo_list'; -import { getCreateSLOFlyoutLazy } from './pages/slo_edit/shared_flyout/get_create_slo_flyout'; import { registerBurnRateRuleType } from './rules/register_burn_rate_rule_type'; -import type { SloPublicSetup, SloPublicStart } from './types'; -import { SloPublicPluginsSetup, SloPublicPluginsStart } from './types'; - -export class SloPlugin - implements Plugin +import type { + SLOPublicPluginsSetup, + SLOPublicPluginsStart, + SLOPublicSetup, + SLOPublicStart, +} from './types'; +import { getLazyWithContextProviders } from './utils/get_lazy_with_context_providers'; + +export class SLOPlugin + implements Plugin { private readonly appUpdater$ = new BehaviorSubject(() => ({})); private experimentalFeatures: ExperimentalFeatures = { ruleFormV2: { enabled: false } }; - constructor(private readonly initContext: PluginInitializerContext) { + constructor(private readonly initContext: PluginInitializerContext) { this.experimentalFeatures = this.initContext.config.get().experimental ?? this.experimentalFeatures; } public setup( - coreSetup: CoreSetup, - pluginsSetup: SloPublicPluginsSetup + core: CoreSetup, + plugins: SLOPublicPluginsSetup ) { const kibanaVersion = this.initContext.env.packageInfo.version; - const sloDetailsLocator = pluginsSetup.share.url.locators.create( - new SloDetailsLocatorDefinition() - ); - const sloEditLocator = pluginsSetup.share.url.locators.create(new SloEditLocatorDefinition()); - const sloListLocator = pluginsSetup.share.url.locators.create(new SloListLocatorDefinition()); + const sloClient = createRepositoryClient(core); + + const sloDetailsLocator = plugins.share.url.locators.create(new SloDetailsLocatorDefinition()); + const sloEditLocator = plugins.share.url.locators.create(new SloEditLocatorDefinition()); + const sloListLocator = plugins.share.url.locators.create(new SloListLocatorDefinition()); const mount = async (params: AppMountParameters) => { const { renderApp } = await import('./application'); - const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + const [coreStart, pluginsStart] = await core.getStartServices(); const { observabilityRuleTypeRegistry } = pluginsStart.observability; return renderApp({ @@ -65,11 +72,12 @@ export class SloPlugin isDev: this.initContext.env.mode.dev, observabilityRuleTypeRegistry, kibanaVersion, - usageCollection: pluginsSetup.usageCollection, + usageCollection: plugins.usageCollection, ObservabilityPageTemplate: pluginsStart.observabilityShared.navigation.PageTemplate, plugins: pluginsStart, isServerless: !!pluginsStart.serverless, experimentalFeatures: this.experimentalFeatures, + sloClient, }); }; const appUpdater$ = this.appUpdater$; @@ -85,17 +93,36 @@ export class SloPlugin keywords: ['observability', 'monitor', 'slos'], }; // Register an application into the side navigation menu - coreSetup.application.register(app); + core.application.register(app); + + const registerRules = async () => { + const [coreStart, pluginsStart] = await core.getStartServices(); + const lazyWithContextProviders = getLazyWithContextProviders({ + core: coreStart, + isDev: this.initContext.env.mode.dev, + kibanaVersion, + observabilityRuleTypeRegistry: pluginsStart.observability.observabilityRuleTypeRegistry, + ObservabilityPageTemplate: pluginsStart.observabilityShared.navigation.PageTemplate, + plugins: pluginsStart, + isServerless: !!plugins.serverless, + experimentalFeatures: this.experimentalFeatures, + sloClient, + }); - registerBurnRateRuleType(pluginsSetup.observability.observabilityRuleTypeRegistry); + registerBurnRateRuleType( + plugins.observability.observabilityRuleTypeRegistry, + lazyWithContextProviders + ); + }; + registerRules(); - const assertPlatinumLicense = async () => { - const licensing = pluginsSetup.licensing; + const registerEmbeddables = async () => { + const licensing = plugins.licensing; const license = await firstValueFrom(licensing.license$); const hasPlatinumLicense = license.hasAtLeast('platinum'); if (hasPlatinumLicense) { - const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + const [coreStart, pluginsStart] = await core.getStartServices(); pluginsStart.dashboard.registerDashboardPanelPlacementSetting( SLO_OVERVIEW_EMBEDDABLE_ID, @@ -106,66 +133,61 @@ export class SloPlugin return { width: 12, height: 8 }; } ); - - pluginsSetup.embeddable.registerReactEmbeddableFactory( - SLO_OVERVIEW_EMBEDDABLE_ID, - async () => { - const { getOverviewEmbeddableFactory } = await import( - './embeddable/slo/overview/slo_embeddable_factory' - ); - return getOverviewEmbeddableFactory(coreSetup.getStartServices); + pluginsStart.dashboard.registerDashboardPanelPlacementSetting( + SLO_BURN_RATE_EMBEDDABLE_ID, + () => { + return { width: 14, height: 7 }; } ); - pluginsSetup.embeddable.registerReactEmbeddableFactory( - SLO_ALERTS_EMBEDDABLE_ID, - async () => { - const { getAlertsEmbeddableFactory } = await import( - './embeddable/slo/alerts/slo_alerts_embeddable_factory' - ); + plugins.embeddable.registerReactEmbeddableFactory(SLO_OVERVIEW_EMBEDDABLE_ID, async () => { + const { getOverviewEmbeddableFactory } = await import( + './embeddable/slo/overview/slo_embeddable_factory' + ); + return getOverviewEmbeddableFactory({ coreStart, pluginsStart, sloClient }); + }); - return getAlertsEmbeddableFactory(coreSetup.getStartServices, kibanaVersion); - } - ); + plugins.embeddable.registerReactEmbeddableFactory(SLO_ALERTS_EMBEDDABLE_ID, async () => { + const { getAlertsEmbeddableFactory } = await import( + './embeddable/slo/alerts/slo_alerts_embeddable_factory' + ); - pluginsSetup.embeddable.registerReactEmbeddableFactory(SLO_ERROR_BUDGET_ID, async () => { - const deps = { ...coreStart, ...pluginsStart }; + return getAlertsEmbeddableFactory({ coreStart, pluginsStart, sloClient, kibanaVersion }); + }); + plugins.embeddable.registerReactEmbeddableFactory(SLO_ERROR_BUDGET_ID, async () => { const { getErrorBudgetEmbeddableFactory } = await import( './embeddable/slo/error_budget/error_budget_react_embeddable_factory' ); - return getErrorBudgetEmbeddableFactory(deps); + return getErrorBudgetEmbeddableFactory({ + coreStart, + pluginsStart, + sloClient, + }); }); - pluginsStart.dashboard.registerDashboardPanelPlacementSetting( - SLO_BURN_RATE_EMBEDDABLE_ID, - () => { - return { width: 14, height: 7 }; - } - ); - pluginsSetup.embeddable.registerReactEmbeddableFactory( - SLO_BURN_RATE_EMBEDDABLE_ID, - async () => { - const deps = { ...coreStart, ...pluginsStart }; - - const { getBurnRateEmbeddableFactory } = await import( - './embeddable/slo/burn_rate/burn_rate_react_embeddable_factory' - ); - return getBurnRateEmbeddableFactory(deps); - } - ); + plugins.embeddable.registerReactEmbeddableFactory(SLO_BURN_RATE_EMBEDDABLE_ID, async () => { + const { getBurnRateEmbeddableFactory } = await import( + './embeddable/slo/burn_rate/burn_rate_react_embeddable_factory' + ); + return getBurnRateEmbeddableFactory({ + coreStart, + pluginsStart, + sloClient, + }); + }); const registerAsyncSloUiActions = async () => { - if (pluginsSetup.uiActions) { + if (plugins.uiActions) { const { registerSloUiActions } = await import('./ui_actions'); - registerSloUiActions(coreSetup, pluginsSetup, pluginsStart); + registerSloUiActions(plugins.uiActions, coreStart, pluginsStart, sloClient); } }; registerAsyncSloUiActions(); } }; - assertPlatinumLicense(); + registerEmbeddables(); return { sloDetailsLocator, @@ -174,19 +196,27 @@ export class SloPlugin }; } - public start(coreStart: CoreStart, pluginsStart: SloPublicPluginsStart) { + public start(core: CoreStart, plugins: SLOPublicPluginsStart) { const kibanaVersion = this.initContext.env.packageInfo.version; + const sloClient = createRepositoryClient(core); + + const lazyWithContextProviders = getLazyWithContextProviders({ + core, + isDev: this.initContext.env.mode.dev, + kibanaVersion, + observabilityRuleTypeRegistry: plugins.observability.observabilityRuleTypeRegistry, + ObservabilityPageTemplate: plugins.observabilityShared.navigation.PageTemplate, + plugins, + isServerless: !!plugins.serverless, + experimentalFeatures: this.experimentalFeatures, + sloClient, + }); + return { - getCreateSLOFlyout: getCreateSLOFlyoutLazy({ - core: coreStart, - isDev: this.initContext.env.mode.dev, - kibanaVersion, - observabilityRuleTypeRegistry: pluginsStart.observability.observabilityRuleTypeRegistry, - ObservabilityPageTemplate: pluginsStart.observabilityShared.navigation.PageTemplate, - plugins: pluginsStart, - isServerless: !!pluginsStart.serverless, - experimentalFeatures: this.experimentalFeatures, - }), + getCreateSLOFlyout: lazyWithContextProviders( + lazy(() => import('./pages/slo_edit/shared_flyout/slo_add_form_flyout')), + { spinnerSize: 'm' } + ), }; } diff --git a/x-pack/plugins/observability_solution/slo/public/rules/register_burn_rate_rule_type.ts b/x-pack/plugins/observability_solution/slo/public/rules/register_burn_rate_rule_type.ts index f843809543620..5861e5ffd9032 100644 --- a/x-pack/plugins/observability_solution/slo/public/rules/register_burn_rate_rule_type.ts +++ b/x-pack/plugins/observability_solution/slo/public/rules/register_burn_rate_rule_type.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { ALERT_REASON } from '@kbn/rule-data-utils'; import { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public/rules/create_observability_rule_type_registry'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { ALERT_REASON, SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { lazy } from 'react'; import { SLO_ID_FIELD, SLO_INSTANCE_ID_FIELD } from '../../common/field_names/slo'; import { validateBurnRateRule } from '../components/burn_rate_rule_editor/validation'; +import { LazyWithContextProviders } from '../utils/get_lazy_with_context_providers'; const sloBurnRateDefaultActionMessage = i18n.translate( 'xpack.slo.rules.burnRate.defaultActionMessage', @@ -47,7 +47,8 @@ const sloBurnRateDefaultRecoveryMessage = i18n.translate( ); export const registerBurnRateRuleType = ( - observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry, + lazyWithContextProviders: LazyWithContextProviders ) => { observabilityRuleTypeRegistry.register({ id: SLO_BURN_RATE_RULE_TYPE_ID, @@ -71,8 +72,8 @@ export const registerBurnRateRuleType = ( requiresAppContext: false, defaultActionMessage: sloBurnRateDefaultActionMessage, defaultRecoveryMessage: sloBurnRateDefaultRecoveryMessage, - alertDetailsAppSection: lazy( - () => import('../components/slo/burn_rate/alert_details/alert_details_app_section') + alertDetailsAppSection: lazyWithContextProviders( + lazy(() => import('../components/slo/burn_rate/alert_details/alert_details_app_section')) ), priority: 100, }); diff --git a/x-pack/plugins/observability_solution/slo/public/types.ts b/x-pack/plugins/observability_solution/slo/public/types.ts index 9e730bd429541..2c66b340df6d3 100644 --- a/x-pack/plugins/observability_solution/slo/public/types.ts +++ b/x-pack/plugins/observability_solution/slo/public/types.ts @@ -4,6 +4,25 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { AiopsPluginStart } from '@kbn/aiops-plugin/public/types'; +import { CasesPublicStart } from '@kbn/cases-plugin/public'; +import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; +import { DashboardStart } from '@kbn/dashboard-plugin/public'; +import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { DiscoverStart } from '@kbn/discover-plugin/public'; +import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { LensPublicStart } from '@kbn/lens-plugin/public'; +import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { + ObservabilityAIAssistantPublicSetup, + ObservabilityAIAssistantPublicStart, +} from '@kbn/observability-ai-assistant-plugin/public'; import { ObservabilityPublicSetup, ObservabilityPublicStart, @@ -12,85 +31,68 @@ import type { ObservabilitySharedPluginSetup, ObservabilitySharedPluginStart, } from '@kbn/observability-shared-plugin/public'; -import { AiopsPluginStart } from '@kbn/aiops-plugin/public/types'; -import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; -import { DashboardStart } from '@kbn/dashboard-plugin/public'; -import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; +import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; +import { DefaultClientOptions, RouteRepositoryClient } from '@kbn/server-route-repository-client'; +import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; +import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; -import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; -import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; -import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public'; -import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; -import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; -import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { CloudStart } from '@kbn/cloud-plugin/public'; +import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { UsageCollectionSetup, UsageCollectionStart, } from '@kbn/usage-collection-plugin/public'; -import { - ObservabilityAIAssistantPublicSetup, - ObservabilityAIAssistantPublicStart, -} from '@kbn/observability-ai-assistant-plugin/public'; -import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import type { LensPublicStart } from '@kbn/lens-plugin/public'; -import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { CasesPublicStart } from '@kbn/cases-plugin/public'; -import type { DiscoverStart } from '@kbn/discover-plugin/public'; -import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { SLORouteRepository } from '../server/routes/get_slo_server_route_repository'; +import { SLOPlugin } from './plugin'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { SloPlugin } from './plugin'; +export type SLORepositoryClient = RouteRepositoryClient; -export interface SloPublicPluginsSetup { +export interface SLOPublicPluginsSetup { data: DataPublicPluginSetup; + embeddable: EmbeddableSetup; + licensing: LicensingPluginSetup; observability: ObservabilityPublicSetup; + observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup; observabilityShared: ObservabilitySharedPluginSetup; - triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; - licensing: LicensingPluginSetup; + presentationUtil: PresentationUtilPluginStart; + serverless?: ServerlessPluginSetup; share: SharePluginSetup; - embeddable: EmbeddableSetup; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; uiActions: UiActionsSetup; - serverless?: ServerlessPluginSetup; - presentationUtil: PresentationUtilPluginStart; - observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup; usageCollection: UsageCollectionSetup; } -export interface SloPublicPluginsStart { +export interface SLOPublicPluginsStart { aiops: AiopsPluginStart; cases: CasesPublicStart; + charts: ChartsPluginStart; cloud?: CloudStart; dashboard: DashboardStart; + data: DataPublicPluginStart; dataViewEditor: DataViewEditorStart; + dataViewFieldEditor: DataViewFieldEditorStart; + dataViews: DataViewsPublicPluginStart; + discover?: DiscoverStart; + embeddable: EmbeddableStart; fieldFormats: FieldFormatsStart; + lens: LensPublicStart; + licensing: LicensingPluginStart; observability: ObservabilityPublicStart; + observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; observabilityShared: ObservabilitySharedPluginStart; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; - spaces?: SpacesPluginStart; - share: SharePluginStart; - licensing: LicensingPluginStart; - embeddable: EmbeddableStart; - uiActions: UiActionsStart; presentationUtil: PresentationUtilPluginStart; serverless?: ServerlessPluginStart; - data: DataPublicPluginStart; - dataViews: DataViewsPublicPluginStart; - observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; - lens: LensPublicStart; - charts: ChartsPluginStart; + share: SharePluginStart; + spaces?: SpacesPluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; + uiActions: UiActionsStart; unifiedSearch: UnifiedSearchPublicPluginStart; usageCollection: UsageCollectionStart; - discover?: DiscoverStart; - dataViewFieldEditor: DataViewFieldEditorStart; } -export type SloPublicSetup = ReturnType; -export type SloPublicStart = ReturnType; +export type SLOPublicSetup = ReturnType; +export type SLOPublicStart = ReturnType; diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx index 68e985d7d5580..7fb4f021329ee 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx @@ -4,23 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import type { CoreSetup } from '@kbn/core/public'; +import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, type UiActionsActionDefinition, } from '@kbn/ui-actions-plugin/public'; -import { EmbeddableApiContext } from '@kbn/presentation-publishing'; -import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; +import { SLOPublicPluginsStart } from '..'; import { ADD_SLO_ALERTS_ACTION_ID, SLO_ALERTS_EMBEDDABLE_ID, } from '../embeddable/slo/alerts/constants'; -import { SloPublicPluginsStart, SloPublicStart } from '..'; +import { SLORepositoryClient } from '../types'; export function createAddAlertsPanelAction( - getStartServices: CoreSetup['getStartServices'] + coreStart: CoreStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient ): UiActionsActionDefinition { return { id: ADD_SLO_ALERTS_ACTION_ID, @@ -32,12 +35,12 @@ export function createAddAlertsPanelAction( }, execute: async ({ embeddable }) => { if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); - const [coreStart, deps] = await getStartServices(); + try { const { openSloConfiguration } = await import( '../embeddable/slo/alerts/slo_alerts_open_configuration' ); - const initialState = await openSloConfiguration(coreStart, deps); + const initialState = await openSloConfiguration(coreStart, pluginsStart, sloClient); embeddable.addNewPanel( { panelType: SLO_ALERTS_EMBEDDABLE_ID, diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_burn_rate_panel_action.tsx b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_burn_rate_panel_action.tsx index 02490cf1bf7f8..c55dd85aa7027 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_burn_rate_panel_action.tsx +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_burn_rate_panel_action.tsx @@ -4,23 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { CoreSetup } from '@kbn/core/public'; +import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, type UiActionsActionDefinition, } from '@kbn/ui-actions-plugin/public'; -import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; -import { SloPublicPluginsStart, SloPublicStart } from '..'; +import { SLOPublicPluginsStart } from '..'; import { ADD_BURN_RATE_ACTION_ID, SLO_BURN_RATE_EMBEDDABLE_ID, } from '../embeddable/slo/burn_rate/constants'; +import { SLORepositoryClient } from '../types'; export function createBurnRatePanelAction( - getStartServices: CoreSetup['getStartServices'] + coreStart: CoreStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient ): UiActionsActionDefinition { return { id: ADD_BURN_RATE_ACTION_ID, @@ -32,12 +35,12 @@ export function createBurnRatePanelAction( }, execute: async ({ embeddable }) => { if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); - const [coreStart, deps] = await getStartServices(); + try { const { openConfiguration } = await import( '../embeddable/slo/burn_rate/open_configuration' ); - const initialState = await openConfiguration(coreStart, deps); + const initialState = await openConfiguration(coreStart, pluginsStart, sloClient); embeddable.addNewPanel( { panelType: SLO_BURN_RATE_EMBEDDABLE_ID, diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx index 9ba0b7a7a8677..6d1c46094b3a2 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx @@ -5,21 +5,25 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import type { CoreSetup } from '@kbn/core/public'; +import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, type UiActionsActionDefinition, } from '@kbn/ui-actions-plugin/public'; -import { EmbeddableApiContext } from '@kbn/presentation-publishing'; -import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; +import { CoreStart } from '@kbn/core/public'; +import { SLOPublicPluginsStart } from '..'; import { ADD_SLO_ERROR_BUDGET_ACTION_ID, SLO_ERROR_BUDGET_ID, } from '../embeddable/slo/error_budget/constants'; -import { SloPublicPluginsStart, SloPublicStart } from '..'; +import { SLORepositoryClient } from '../types'; + export function createAddErrorBudgetPanelAction( - getStartServices: CoreSetup['getStartServices'] + coreStart: CoreStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient ): UiActionsActionDefinition { return { id: ADD_SLO_ERROR_BUDGET_ACTION_ID, @@ -31,12 +35,11 @@ export function createAddErrorBudgetPanelAction( }, execute: async ({ embeddable }) => { if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); - const [coreStart, deps] = await getStartServices(); try { const { openSloConfiguration } = await import( '../embeddable/slo/error_budget/error_budget_open_configuration' ); - const initialState = await openSloConfiguration(coreStart, deps); + const initialState = await openSloConfiguration(coreStart, pluginsStart, sloClient); embeddable.addNewPanel( { panelType: SLO_ERROR_BUDGET_ID, diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx index c4b6e5009382d..3cb1bf49d55e9 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx @@ -4,23 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import type { CoreSetup } from '@kbn/core/public'; +import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, type UiActionsActionDefinition, } from '@kbn/ui-actions-plugin/public'; -import { EmbeddableApiContext } from '@kbn/presentation-publishing'; -import { COMMON_OBSERVABILITY_GROUPING } from '@kbn/observability-shared-plugin/common'; +import { SLOPublicPluginsStart } from '..'; import { ADD_SLO_OVERVIEW_ACTION_ID, SLO_OVERVIEW_EMBEDDABLE_ID, } from '../embeddable/slo/overview/constants'; -import { SloPublicPluginsStart, SloPublicStart } from '..'; +import { SLORepositoryClient } from '../types'; export function createOverviewPanelAction( - getStartServices: CoreSetup['getStartServices'] + coreStart: CoreStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient ): UiActionsActionDefinition { return { id: ADD_SLO_OVERVIEW_ACTION_ID, @@ -32,12 +35,12 @@ export function createOverviewPanelAction( }, execute: async ({ embeddable }) => { if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); - const [coreStart, deps] = await getStartServices(); + try { const { openSloConfiguration } = await import( '../embeddable/slo/overview/slo_overview_open_configuration' ); - const initialState = await openSloConfiguration(coreStart, deps); + const initialState = await openSloConfiguration(coreStart, pluginsStart, sloClient); embeddable.addNewPanel( { panelType: SLO_OVERVIEW_EMBEDDABLE_ID, diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts b/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts index 26411f02e753d..2e5e69bd02ee2 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts @@ -5,27 +5,33 @@ * 2.0. */ +import type { CoreStart } from '@kbn/core/public'; import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; -import type { CoreSetup } from '@kbn/core/public'; -import { createOverviewPanelAction } from './create_overview_panel_action'; -import { createAddErrorBudgetPanelAction } from './create_error_budget_action'; +import { UiActionsPublicSetup } from '@kbn/ui-actions-plugin/public/plugin'; +import { SLOPublicPluginsStart } from '..'; +import { SLORepositoryClient } from '../types'; import { createAddAlertsPanelAction } from './create_alerts_panel_action'; -import { SloPublicPluginsStart, SloPublicStart, SloPublicPluginsSetup } from '..'; import { createBurnRatePanelAction } from './create_burn_rate_panel_action'; +import { createAddErrorBudgetPanelAction } from './create_error_budget_action'; +import { createOverviewPanelAction } from './create_overview_panel_action'; export function registerSloUiActions( - core: CoreSetup, - pluginsSetup: SloPublicPluginsSetup, - pluginsStart: SloPublicPluginsStart + uiActions: UiActionsPublicSetup, + coreStart: CoreStart, + pluginsStart: SLOPublicPluginsStart, + sloClient: SLORepositoryClient ) { - const { uiActions } = pluginsSetup; const { serverless, cloud } = pluginsStart; // Initialize actions - const addOverviewPanelAction = createOverviewPanelAction(core.getStartServices); - const addErrorBudgetPanelAction = createAddErrorBudgetPanelAction(core.getStartServices); - const addAlertsPanelAction = createAddAlertsPanelAction(core.getStartServices); - const addBurnRatePanelAction = createBurnRatePanelAction(core.getStartServices); + const addOverviewPanelAction = createOverviewPanelAction(coreStart, pluginsStart, sloClient); + const addErrorBudgetPanelAction = createAddErrorBudgetPanelAction( + coreStart, + pluginsStart, + sloClient + ); + const addAlertsPanelAction = createAddAlertsPanelAction(coreStart, pluginsStart, sloClient); + const addBurnRatePanelAction = createBurnRatePanelAction(coreStart, pluginsStart, sloClient); // Assign triggers // Only register these actions in stateful kibana, and the serverless observability project diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/get_create_slo_flyout.tsx b/x-pack/plugins/observability_solution/slo/public/utils/get_lazy_with_context_providers.tsx similarity index 50% rename from x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/get_create_slo_flyout.tsx rename to x-pack/plugins/observability_solution/slo/public/utils/get_lazy_with_context_providers.tsx index 88e326082c9d8..a43aa9e7bff59 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_edit/shared_flyout/get_create_slo_flyout.tsx +++ b/x-pack/plugins/observability_solution/slo/public/utils/get_lazy_with_context_providers.tsx @@ -5,49 +5,52 @@ * 2.0. */ -import React from 'react'; -import { QueryClientProvider } from '@tanstack/react-query'; -import { QueryClient } from '@tanstack/react-query'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiLoadingSpinnerProps } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; -import { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { RecursivePartial } from '@kbn/utility-types'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public'; -import { ExperimentalFeatures } from '../../../../common/config'; -import { CreateSLOForm } from '../types'; -import { PluginContext } from '../../../context/plugin_context'; -import { SloPublicPluginsStart } from '../../../types'; -import { SloAddFormFlyout } from './slo_form'; +import { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React, { Suspense } from 'react'; +import { ExperimentalFeatures } from '../../common/config'; +import { PluginContext } from '../context/plugin_context'; +import { SLOPublicPluginsStart, SLORepositoryClient } from '../types'; -export const getCreateSLOFlyoutLazy = ({ - core, - plugins, - observabilityRuleTypeRegistry, - ObservabilityPageTemplate, - isDev, - kibanaVersion, - isServerless, - experimentalFeatures, -}: { +interface Props { core: CoreStart; - plugins: SloPublicPluginsStart; + plugins: SLOPublicPluginsStart; observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; ObservabilityPageTemplate: React.ComponentType; isDev?: boolean; kibanaVersion: string; isServerless?: boolean; experimentalFeatures: ExperimentalFeatures; -}) => { - return ({ - onClose, - initialValues, - }: { - onClose: () => void; - initialValues?: RecursivePartial; - }) => { + sloClient: SLORepositoryClient; +} + +export type LazyWithContextProviders = ReturnType; + +interface Options { + spinnerSize?: EuiLoadingSpinnerProps['size']; +} + +export const getLazyWithContextProviders = + ({ + core, + plugins, + observabilityRuleTypeRegistry, + ObservabilityPageTemplate, + isDev, + kibanaVersion, + isServerless, + experimentalFeatures, + sloClient, + }: Props) => + (LazyComponent: React.LazyExoticComponent, options?: Options): React.FunctionComponent => { + const { spinnerSize = 'xl' } = options ?? {}; const queryClient = new QueryClient(); - return ( + return (props) => ( - + }> + + ); }; -}; + +function LoadingSpinner({ size }: { size: EuiLoadingSpinnerProps['size'] }) { + return ( + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx index 8785772608b55..8b6e951f9c97c 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx @@ -4,17 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { ComponentType } from 'react'; -import { of } from 'rxjs'; -import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { AppMountParameters } from '@kbn/core-application-browser'; -import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { CoreTheme } from '@kbn/core-theme-browser'; -import { MemoryRouter } from 'react-router-dom'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { casesFeatureId, sloFeatureId } from '@kbn/observability-shared-plugin/common'; -import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React, { ComponentType } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { of } from 'rxjs'; import { PluginContext } from '../context/plugin_context'; +import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; +import { SLORepositoryClient } from '../types'; export function KibanaReactStorybookDecorator(Story: ComponentType) { const queryClient = new QueryClient(); @@ -105,6 +106,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { appMountParameters, observabilityRuleTypeRegistry, ObservabilityPageTemplate: KibanaPageTemplate, + sloClient: {} as SLORepositoryClient, }} > diff --git a/x-pack/plugins/observability_solution/slo/public/utils/test_helper.tsx b/x-pack/plugins/observability_solution/slo/public/utils/test_helper.tsx index 014644d973b74..fd735d94c4a98 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability_solution/slo/public/utils/test_helper.tsx @@ -16,7 +16,9 @@ import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render as testLibRender } from '@testing-library/react'; import React from 'react'; +import { DefaultClientOptions, createRepositoryClient } from '@kbn/server-route-repository-client'; import { PluginContext } from '../context/plugin_context'; +import type { SLORouteRepository } from '../../server/routes/get_slo_server_route_repository'; const appMountParameters = { setHeaderActionMenu: () => {} } as unknown as AppMountParameters; const observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistryMock(); @@ -39,6 +41,8 @@ const queryClient = new QueryClient({ }, }); +const sloClient = createRepositoryClient(core); + export const render = (component: React.ReactNode) => { return testLibRender( // @ts-ignore @@ -60,6 +64,7 @@ export const render = (component: React.ReactNode) => { appMountParameters, observabilityRuleTypeRegistry, ObservabilityPageTemplate: KibanaPageTemplate, + sloClient, }} > diff --git a/x-pack/plugins/observability_solution/slo/server/errors/errors.ts b/x-pack/plugins/observability_solution/slo/server/errors/errors.ts index eaec36e66d08b..66718f64519d6 100644 --- a/x-pack/plugins/observability_solution/slo/server/errors/errors.ts +++ b/x-pack/plugins/observability_solution/slo/server/errors/errors.ts @@ -7,20 +7,18 @@ /* eslint-disable max-classes-per-file */ -export class ObservabilityError extends Error { +export class SLOError extends Error { constructor(message?: string) { super(message); this.name = this.constructor.name; } } -export class SLONotFound extends ObservabilityError {} -export class SLOIdConflict extends ObservabilityError {} +export class SLONotFound extends SLOError {} +export class SLOIdConflict extends SLOError {} -export class InvalidQueryError extends ObservabilityError {} -export class InternalQueryError extends ObservabilityError {} -export class NotSupportedError extends ObservabilityError {} -export class IllegalArgumentError extends ObservabilityError {} -export class InvalidTransformError extends ObservabilityError {} +export class InternalQueryError extends SLOError {} +export class IllegalArgumentError extends SLOError {} +export class InvalidTransformError extends SLOError {} -export class SecurityException extends ObservabilityError {} +export class SecurityException extends SLOError {} diff --git a/x-pack/plugins/observability_solution/slo/server/errors/handler.ts b/x-pack/plugins/observability_solution/slo/server/errors/handler.ts index c10f1d98c083e..2aeebefe2a06c 100644 --- a/x-pack/plugins/observability_solution/slo/server/errors/handler.ts +++ b/x-pack/plugins/observability_solution/slo/server/errors/handler.ts @@ -5,20 +5,33 @@ * 2.0. */ -import { ObservabilityError, SecurityException, SLOIdConflict, SLONotFound } from './errors'; +import { Boom, badRequest, conflict, forbidden, notFound } from '@hapi/boom'; +import { SLOError, SecurityException, SLOIdConflict, SLONotFound } from './errors'; -export function getHTTPResponseCode(error: ObservabilityError): number { +function handleSLOError(error: SLOError): Boom { if (error instanceof SLONotFound) { - return 404; + return notFound(error.message); } if (error instanceof SLOIdConflict) { - return 409; + return conflict(error.message); } if (error instanceof SecurityException) { - return 403; + return forbidden(error.message); } - return 400; + return badRequest(error.message); +} + +export async function executeWithErrorHandler(fn: () => Promise): Promise { + try { + return await fn(); + } catch (error) { + if (error instanceof SLOError) { + throw handleSLOError(error); + } + + throw error; + } } diff --git a/x-pack/plugins/observability_solution/slo/server/index.ts b/x-pack/plugins/observability_solution/slo/server/index.ts index 5d6ccadb7f323..76806ac4d16cd 100644 --- a/x-pack/plugins/observability_solution/slo/server/index.ts +++ b/x-pack/plugins/observability_solution/slo/server/index.ts @@ -5,23 +5,20 @@ * 2.0. */ -import { PluginInitializerContext } from '@kbn/core/server'; -import { configSchema } from '../common/config'; +import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; +import { SLOConfig, configSchema } from '../common/config'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. -export async function plugin(initializerContext: PluginInitializerContext) { - const { SloPlugin } = await import('./plugin'); - return new SloPlugin(initializerContext); +export async function plugin(ctx: PluginInitializerContext) { + const { SLOPlugin } = await import('./plugin'); + return new SLOPlugin(ctx); } -export type { PluginSetup, PluginStart } from './plugin'; - -export const config = { +export const config: PluginConfigDescriptor = { schema: configSchema, exposeToBrowser: { experimental: true, }, }; -export type { SloConfig } from '../common/config'; diff --git a/x-pack/plugins/observability_solution/slo/server/plugin.ts b/x-pack/plugins/observability_solution/slo/server/plugin.ts index a2e4229b2b952..d7d002d26aa03 100644 --- a/x-pack/plugins/observability_solution/slo/server/plugin.ts +++ b/x-pack/plugins/observability_solution/slo/server/plugin.ts @@ -5,79 +5,58 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import { - PluginInitializerContext, CoreSetup, CoreStart, DEFAULT_APP_CATEGORIES, - Plugin, Logger, + Plugin, + PluginInitializerContext, SavedObjectsClient, } from '@kbn/core/server'; -import { PluginSetupContract, PluginStartContract } from '@kbn/alerting-plugin/server'; -import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; -import { - RuleRegistryPluginSetupContract, - RuleRegistryPluginStartContract, -} from '@kbn/rule-registry-plugin/server'; -import { - TaskManagerSetupContract, - TaskManagerStartContract, -} from '@kbn/task-manager-plugin/server'; -import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; -import { CloudSetup } from '@kbn/cloud-plugin/server'; -import { SharePluginSetup } from '@kbn/share-plugin/server'; -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; -import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; -import { AlertsLocatorDefinition } from '@kbn/observability-plugin/common'; -import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; -import { sloFeatureId } from '@kbn/observability-plugin/common'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { AlertsLocatorDefinition, sloFeatureId } from '@kbn/observability-plugin/common'; +import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { mapValues } from 'lodash'; import { registerSloUsageCollector } from './lib/collectors/register'; -import { SloOrphanSummaryCleanupTask } from './services/tasks/orphan_summary_cleanup_task'; -import { slo, SO_SLO_TYPE } from './saved_objects'; -import { DefaultResourceInstaller, DefaultSLOInstaller } from './services'; import { registerBurnRateRule } from './lib/rules/register_burn_rate_rule'; -import { SloConfig } from '.'; -import { registerRoutes } from './routes/register_routes'; import { getSloServerRouteRepository } from './routes/get_slo_server_route_repository'; -import { sloSettings, SO_SLO_SETTINGS_TYPE } from './saved_objects/slo_settings'; - -export type SloPluginSetup = ReturnType; - -export interface PluginSetup { - alerting: PluginSetupContract; - ruleRegistry: RuleRegistryPluginSetupContract; - share: SharePluginSetup; - features: FeaturesPluginSetup; - taskManager: TaskManagerSetupContract; - spaces?: SpacesPluginSetup; - cloud?: CloudSetup; - usageCollection: UsageCollectionSetup; -} - -export interface PluginStart { - alerting: PluginStartContract; - taskManager: TaskManagerStartContract; - spaces?: SpacesPluginStart; - ruleRegistry: RuleRegistryPluginStartContract; - dataViews: DataViewsServerPluginStart; -} +import { registerServerRoutes } from './routes/register_routes'; +import { SLORoutesDependencies } from './routes/types'; +import { SO_SLO_TYPE, slo } from './saved_objects'; +import { SO_SLO_SETTINGS_TYPE, sloSettings } from './saved_objects/slo_settings'; +import { DefaultResourceInstaller, DefaultSLOInstaller } from './services'; +import { SloOrphanSummaryCleanupTask } from './services/tasks/orphan_summary_cleanup_task'; +import type { + SLOConfig, + SLOPluginSetupDependencies, + SLOPluginStartDependencies, + SLOServerSetup, + SLOServerStart, +} from './types'; const sloRuleTypes = [SLO_BURN_RATE_RULE_TYPE_ID]; -export class SloPlugin implements Plugin { +export class SLOPlugin + implements + Plugin +{ private readonly logger: Logger; + private readonly config: SLOConfig; + private readonly isServerless: boolean; private sloOrphanCleanupTask?: SloOrphanSummaryCleanupTask; constructor(private readonly initContext: PluginInitializerContext) { - this.initContext = initContext; - this.logger = initContext.logger.get(); + this.logger = this.initContext.logger.get(); + this.config = this.initContext.config.get(); + this.isServerless = this.initContext.env.packageInfo.buildFlavor === 'serverless'; } - public setup(core: CoreSetup, plugins: PluginSetup) { - const config = this.initContext.config.get(); + public setup( + core: CoreSetup, + plugins: SLOPluginSetupDependencies + ): SLOServerSetup { const alertsLocator = plugins.share.url.locators.create(new AlertsLocatorDefinition()); const savedObjectTypes = [SO_SLO_TYPE, SO_SLO_SETTINGS_TYPE]; @@ -144,36 +123,24 @@ export class SloPlugin implements Plugin { registerSloUsageCollector(plugins.usageCollection); - registerRoutes({ + const routeHandlerPlugins = mapValues(plugins, (value, key) => { + return { + setup: value, + start: () => + core.getStartServices().then(([, pluginStart]) => { + return pluginStart[key as keyof SLOPluginStartDependencies]; + }), + }; + }) as SLORoutesDependencies['plugins']; + + registerServerRoutes({ core, - config, dependencies: { - pluginsSetup: { - ...plugins, - core, - }, - getDataViewsStart: async () => { - const [, pluginStart] = await core.getStartServices(); - return pluginStart.dataViews; - }, - getSpacesStart: async () => { - const [, pluginStart] = await core.getStartServices(); - return pluginStart.spaces; - }, - ruleDataService, - getRulesClientWithRequest: async (request) => { - const [, pluginStart] = await core.getStartServices(); - return pluginStart.alerting.getRulesClientWithRequest(request); - }, - getRacClientWithRequest: async (request) => { - const [, pluginStart] = await core.getStartServices(); - return pluginStart.ruleRegistry.getRacClientWithRequest(request); - }, + corePlugins: core, + plugins: routeHandlerPlugins, }, logger: this.logger, - repository: getSloServerRouteRepository({ - isServerless: this.initContext.env.packageInfo.buildFlavor === 'serverless', - }), + repository: getSloServerRouteRepository({ isServerless: this.isServerless }), }); core @@ -191,18 +158,20 @@ export class SloPlugin implements Plugin { this.sloOrphanCleanupTask = new SloOrphanSummaryCleanupTask( plugins.taskManager, this.logger, - config + this.config ); + + return {}; } - public start(core: CoreStart, plugins: PluginStart) { + public start(core: CoreStart, plugins: SLOPluginStartDependencies): SLOServerStart { const internalSoClient = new SavedObjectsClient(core.savedObjects.createInternalRepository()); const internalEsClient = core.elasticsearch.client.asInternalUser; this.sloOrphanCleanupTask ?.start(plugins.taskManager, internalSoClient, internalEsClient) .catch(() => {}); - } - public stop() {} + return {}; + } } diff --git a/x-pack/plugins/observability_solution/slo/server/routes/create_slo_server_route.ts b/x-pack/plugins/observability_solution/slo/server/routes/create_slo_server_route.ts index 762b5b369f6e6..6d1b762f1dca5 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/create_slo_server_route.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/create_slo_server_route.ts @@ -5,9 +5,6 @@ * 2.0. */ import { createServerRouteFactory } from '@kbn/server-route-repository'; -import { SloRouteCreateOptions, SloRouteHandlerResources } from './types'; +import { SLORouteHandlerResources } from './types'; -export const createSloServerRoute = createServerRouteFactory< - SloRouteHandlerResources, - SloRouteCreateOptions ->(); +export const createSloServerRoute = createServerRouteFactory(); diff --git a/x-pack/plugins/observability_solution/slo/server/routes/get_slo_server_route_repository.ts b/x-pack/plugins/observability_solution/slo/server/routes/get_slo_server_route_repository.ts index b04b6209054a9..f4205c98363d8 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/get_slo_server_route_repository.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/get_slo_server_route_repository.ts @@ -8,9 +8,7 @@ import { getSloRouteRepository } from './slo/route'; export function getSloServerRouteRepository({ isServerless }: { isServerless?: boolean } = {}) { - return { - ...getSloRouteRepository(isServerless), - }; + return getSloRouteRepository(isServerless); } -export type SloServerRouteRepository = ReturnType; +export type SLORouteRepository = ReturnType; diff --git a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts index 6a7d2d08bdd1b..fd0b18c210041 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts @@ -4,136 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { errors } from '@elastic/elasticsearch'; -import Boom from '@hapi/boom'; -import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; -import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server'; -import { - AlertsClient, - RuleDataPluginService, - RuleRegistryPluginSetupContract, -} from '@kbn/rule-registry-plugin/server'; -import { - IoTsParamsObject, - decodeRequestParams, - stripNullishRequestParameters, - parseEndpoint, - passThroughValidationObject, -} from '@kbn/server-route-repository'; -import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; -import axios from 'axios'; -import * as t from 'io-ts'; -import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; -import { SloConfig } from '..'; -import { getHTTPResponseCode, ObservabilityError } from '../errors'; -import { SloRequestHandlerContext } from '../types'; -import { AbstractSloServerRouteRepository } from './types'; +import { CoreSetup, Logger } from '@kbn/core/server'; +import { ServerRoute, registerRoutes } from '@kbn/server-route-repository'; +import { ServerRouteCreateOptions } from '@kbn/server-route-repository-utils'; +import { SLORoutesDependencies } from './types'; interface RegisterRoutes { - config: SloConfig; core: CoreSetup; - repository: AbstractSloServerRouteRepository; + repository: Record>; logger: Logger; - dependencies: RegisterRoutesDependencies; + dependencies: SLORoutesDependencies; } -export interface RegisterRoutesDependencies { - pluginsSetup: { - core: CoreSetup; - ruleRegistry: RuleRegistryPluginSetupContract; - }; - getSpacesStart: () => Promise; - ruleDataService: RuleDataPluginService; - getRulesClientWithRequest: (request: KibanaRequest) => Promise; - getRacClientWithRequest: (request: KibanaRequest) => Promise; - getDataViewsStart: () => Promise; -} - -export function registerRoutes({ config, repository, core, logger, dependencies }: RegisterRoutes) { - const routes = Object.values(repository); - - const router = core.http.createRouter(); - - routes.forEach((route) => { - const { endpoint, options, handler, params } = route; - const { pathname, method } = parseEndpoint(endpoint); - - (router[method] as RouteRegistrar)( - { - path: pathname, - validate: passThroughValidationObject, - options, - }, - async (context, request, response) => { - try { - const decodedParams = decodeRequestParams( - stripNullishRequestParameters({ - params: request.params, - body: request.body, - query: request.query, - }), - (params as IoTsParamsObject) ?? t.strict({}) - ); - - const data = await handler({ - config, - context, - request, - logger, - params: decodedParams, - dependencies, - }); - - if (data === undefined) { - return response.noContent(); - } - - return response.ok({ body: data }); - } catch (error) { - if (error instanceof ObservabilityError) { - logger.error(error.message); - return response.customError({ - statusCode: getHTTPResponseCode(error), - body: { - message: error.message, - }, - }); - } - - if (axios.isAxiosError(error)) { - logger.error(error); - return response.customError({ - statusCode: error.response?.status || 500, - body: { - message: error.message, - }, - }); - } - - if (Boom.isBoom(error)) { - logger.error(error.output.payload.message); - return response.customError({ - statusCode: error.output.statusCode, - body: { message: error.output.payload.message }, - }); - } - - logger.error(error); - const opts = { - statusCode: 500, - body: { - message: error.message, - }, - }; - - if (error instanceof errors.RequestAbortedError) { - opts.statusCode = 499; - opts.body.message = 'Client closed request'; - } - - return response.customError(opts); - } - } - ); +export function registerServerRoutes({ repository, core, logger, dependencies }: RegisterRoutes) { + registerRoutes({ + repository, + dependencies, + core, + logger, }); } diff --git a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts index 838adc72cfd08..9e63a4b02fe7b 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts @@ -7,32 +7,30 @@ import { errors } from '@elastic/elasticsearch'; import { failedDependency, forbidden } from '@hapi/boom'; +import { KibanaRequest } from '@kbn/core-http-server'; import { + PutSLOSettingsParams, createSLOParamsSchema, deleteSLOInstancesParamsSchema, deleteSLOParamsSchema, fetchHistoricalSummaryParamsSchema, - fetchHistoricalSummaryResponseSchema, fetchSLOHealthParamsSchema, - findSloDefinitionsParamsSchema, findSLOGroupsParamsSchema, findSLOParamsSchema, + findSloDefinitionsParamsSchema, getPreviewDataParamsSchema, getSLOBurnRatesParamsSchema, getSLOInstancesParamsSchema, getSLOParamsSchema, manageSLOParamsSchema, putSLOServerlessSettingsParamsSchema, - PutSLOSettingsParams, putSLOSettingsParamsSchema, resetSLOParamsSchema, updateSLOParamsSchema, } from '@kbn/slo-schema'; import { getOverviewParamsSchema } from '@kbn/slo-schema/src/rest_specs/routes/get_overview'; -import { KibanaRequest } from '@kbn/core-http-server'; -import { RegisterRoutesDependencies } from '../register_routes'; -import { GetSLOsOverview } from '../../services/get_slos_overview'; import type { IndicatorTypes } from '../../domain/models'; +import { executeWithErrorHandler } from '../../errors'; import { CreateSLO, DefaultBurnRatesClient, @@ -54,6 +52,7 @@ import { getGlobalDiagnosis } from '../../services/get_diagnosis'; import { GetPreviewData } from '../../services/get_preview_data'; import { GetSLOInstances } from '../../services/get_slo_instances'; import { GetSLOSuggestions } from '../../services/get_slo_suggestions'; +import { GetSLOsOverview } from '../../services/get_slos_overview'; import { DefaultHistoricalSummaryClient } from '../../services/historical_summary_client'; import { ManageSLO } from '../../services/manage_slo'; import { ResetSLO } from '../../services/reset_slo'; @@ -71,8 +70,8 @@ import { TimesliceMetricTransformGenerator, TransformGenerator, } from '../../services/transform_generators'; -import type { SloRequestHandlerContext } from '../../types'; import { createSloServerRoute } from '../create_slo_server_route'; +import { SLORoutesDependencies } from '../types'; const transformGenerators: Record = { 'sli.apm.transactionDuration': new ApmTransactionDurationTransformGenerator(), @@ -84,17 +83,17 @@ const transformGenerators: Record = { 'sli.metric.timeslice': new TimesliceMetricTransformGenerator(), }; -const assertPlatinumLicense = async (context: SloRequestHandlerContext) => { - const licensing = await context.licensing; - const hasCorrectLicense = licensing.license.hasAtLeast('platinum'); +const assertPlatinumLicense = async (plugins: SLORoutesDependencies['plugins']) => { + const licensing = await plugins.licensing.start(); + const hasCorrectLicense = (await licensing.getLicense()).hasAtLeast('platinum'); if (!hasCorrectLicense) { throw forbidden('Platinum license or higher is needed to make use of this feature.'); } }; -const getSpaceId = async (deps: RegisterRoutesDependencies, request: KibanaRequest) => { - const spaces = await deps.getSpacesStart(); +const getSpaceId = async (plugins: SLORoutesDependencies['plugins'], request: KibanaRequest) => { + const spaces = await plugins.spaces.start(); return (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; }; @@ -105,19 +104,19 @@ const createSLORoute = createSloServerRoute({ access: 'public', }, params: createSLOParamsSchema, - handler: async ({ context, params, logger, dependencies, request }) => { - await assertPlatinumLicense(context); + handler: async ({ context, response, params, logger, request, plugins, corePlugins }) => { + await assertPlatinumLicense(plugins); - const dataViews = await dependencies.getDataViewsStart(); + const dataViews = await plugins.dataViews.start(); const core = await context.core; const scopedClusterClient = core.elasticsearch.client; const esClient = core.elasticsearch.client.asCurrentUser; - const basePath = dependencies.pluginsSetup.core.http.basePath; const soClient = core.savedObjects.client; + const basePath = corePlugins.http.basePath; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const [spaceId, dataViewsService] = await Promise.all([ - getSpaceId(dependencies, request), + getSpaceId(plugins, request), dataViews.dataViewsServiceFactory(soClient, esClient), ]); const transformManager = new DefaultTransformManager( @@ -143,7 +142,7 @@ const createSLORoute = createSloServerRoute({ basePath ); - return await createSLO.execute(params.body); + return await executeWithErrorHandler(() => createSLO.execute(params.body)); }, }); @@ -154,13 +153,12 @@ const inspectSLORoute = createSloServerRoute({ access: 'internal', }, params: createSLOParamsSchema, - handler: async ({ context, params, logger, dependencies, request }) => { - await assertPlatinumLicense(context); + handler: async ({ context, params, logger, request, plugins, corePlugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const dataViews = await dependencies.getDataViewsStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; - const basePath = dependencies.pluginsSetup.core.http.basePath; + const dataViews = await plugins.dataViews.start(); + const spaceId = await getSpaceId(plugins, request); + const basePath = corePlugins.http.basePath; const core = await context.core; const scopedClusterClient = core.elasticsearch.client; const esClient = core.elasticsearch.client.asCurrentUser; @@ -191,7 +189,7 @@ const inspectSLORoute = createSloServerRoute({ basePath ); - return createSLO.inspect(params.body); + return await executeWithErrorHandler(() => createSLO.inspect(params.body)); }, }); @@ -202,14 +200,13 @@ const updateSLORoute = createSloServerRoute({ access: 'public', }, params: updateSLOParamsSchema, - handler: async ({ context, request, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ context, request, params, logger, plugins, corePlugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; - const dataViews = await dependencies.getDataViewsStart(); + const spaceId = await getSpaceId(plugins, request); + const dataViews = await plugins.dataViews.start(); - const basePath = dependencies.pluginsSetup.core.http.basePath; + const basePath = corePlugins.http.basePath; const core = await context.core; const scopedClusterClient = core.elasticsearch.client; const esClient = core.elasticsearch.client.asCurrentUser; @@ -240,9 +237,7 @@ const updateSLORoute = createSloServerRoute({ basePath ); - const response = await updateSLO.execute(params.path.id, params.body); - - return response; + return await executeWithErrorHandler(() => updateSLO.execute(params.path.id, params.body)); }, }); @@ -253,18 +248,19 @@ const deleteSLORoute = createSloServerRoute({ access: 'public', }, params: deleteSLOParamsSchema, - handler: async ({ request, context, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ request, response, context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; - const dataViews = await dependencies.getDataViewsStart(); + const spaceId = await getSpaceId(plugins, request); + const dataViews = await plugins.dataViews.start(); const core = await context.core; const scopedClusterClient = core.elasticsearch.client; const esClient = core.elasticsearch.client.asCurrentUser; const soClient = core.savedObjects.client; - const rulesClient = await dependencies.getRulesClientWithRequest(request); + + const alerting = await plugins.alerting.start(); + const rulesClient = await alerting.getRulesClientWithRequest(request); const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); @@ -292,7 +288,8 @@ const deleteSLORoute = createSloServerRoute({ rulesClient ); - await deleteSLO.execute(params.path.id); + await executeWithErrorHandler(() => deleteSLO.execute(params.path.id)); + return response.noContent(); }, }); @@ -303,11 +300,10 @@ const getSLORoute = createSloServerRoute({ access: 'public', }, params: getSLOParamsSchema, - handler: async ({ request, context, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ request, context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const spaceId = await getSpaceId(plugins, request); const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; @@ -317,7 +313,9 @@ const getSLORoute = createSloServerRoute({ const defintionClient = new SloDefinitionClient(repository, esClient, logger); const getSLO = new GetSLO(defintionClient, summaryClient); - return await getSLO.execute(params.path.id, spaceId, params.query); + return await executeWithErrorHandler(() => + getSLO.execute(params.path.id, spaceId, params.query) + ); }, }); @@ -328,12 +326,11 @@ const enableSLORoute = createSloServerRoute({ access: 'public', }, params: manageSLOParamsSchema, - handler: async ({ request, context, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ request, response, context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; - const dataViews = await dependencies.getDataViewsStart(); + const spaceId = await getSpaceId(plugins, request); + const dataViews = await plugins.dataViews.start(); const core = await context.core; const scopedClusterClient = core.elasticsearch.client; @@ -356,9 +353,9 @@ const enableSLORoute = createSloServerRoute({ const manageSLO = new ManageSLO(repository, transformManager, summaryTransformManager); - const response = await manageSLO.enable(params.path.id); + await executeWithErrorHandler(() => manageSLO.enable(params.path.id)); - return response; + return response.noContent(); }, }); @@ -369,12 +366,11 @@ const disableSLORoute = createSloServerRoute({ access: 'public', }, params: manageSLOParamsSchema, - handler: async ({ request, context, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ response, request, context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; - const dataViews = await dependencies.getDataViewsStart(); + const spaceId = await getSpaceId(plugins, request); + const dataViews = await plugins.dataViews.start(); const core = await context.core; const scopedClusterClient = core.elasticsearch.client; @@ -397,9 +393,8 @@ const disableSLORoute = createSloServerRoute({ const manageSLO = new ManageSLO(repository, transformManager, summaryTransformManager); - const response = await manageSLO.disable(params.path.id); - - return response; + await executeWithErrorHandler(() => manageSLO.disable(params.path.id)); + return response.noContent(); }, }); @@ -410,17 +405,16 @@ const resetSLORoute = createSloServerRoute({ access: 'public', }, params: resetSLOParamsSchema, - handler: async ({ context, request, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ context, request, params, logger, plugins, corePlugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const dataViews = await dependencies.getDataViewsStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const dataViews = await plugins.dataViews.start(); + const spaceId = await getSpaceId(plugins, request); const core = await context.core; const scopedClusterClient = core.elasticsearch.client; const soClient = core.savedObjects.client; const esClient = core.elasticsearch.client.asCurrentUser; - const basePath = dependencies.pluginsSetup.core.http.basePath; + const basePath = corePlugins.http.basePath; const dataViewsService = await dataViews.dataViewsServiceFactory(soClient, esClient); const repository = new KibanaSavedObjectsSLORepository(soClient, logger); @@ -448,9 +442,7 @@ const resetSLORoute = createSloServerRoute({ basePath ); - const response = await resetSLO.execute(params.path.id); - - return response; + return await executeWithErrorHandler(() => resetSLO.execute(params.path.id)); }, }); @@ -461,11 +453,10 @@ const findSLORoute = createSloServerRoute({ access: 'public', }, params: findSLOParamsSchema, - handler: async ({ context, request, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ context, request, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; + const spaceId = await getSpaceId(plugins, request); const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); @@ -473,7 +464,7 @@ const findSLORoute = createSloServerRoute({ const findSLO = new FindSLO(repository, summarySearchClient); - return await findSLO.execute(params?.query ?? {}); + return await executeWithErrorHandler(() => findSLO.execute(params?.query ?? {})); }, }); @@ -484,16 +475,15 @@ const findSLOGroupsRoute = createSloServerRoute({ access: 'internal', }, params: findSLOGroupsParamsSchema, - handler: async ({ context, request, params, logger, dependencies }) => { - await assertPlatinumLicense(context); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService.getActiveSpace(request))?.id ?? 'default'; + handler: async ({ context, request, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); + + const spaceId = await getSpaceId(plugins, request); const soClient = (await context.core).savedObjects.client; const coreContext = context.core; const esClient = (await coreContext).elasticsearch.client.asCurrentUser; const findSLOGroups = new FindSLOGroups(esClient, soClient, logger, spaceId); - const response = await findSLOGroups.execute(params?.query ?? {}); - return response; + return await executeWithErrorHandler(() => findSLOGroups.execute(params?.query ?? {})); }, }); @@ -503,12 +493,12 @@ const getSLOSuggestionsRoute = createSloServerRoute({ tags: ['access:slo_read'], access: 'internal', }, - handler: async ({ context }) => { - await assertPlatinumLicense(context); + handler: async ({ context, plugins }) => { + await assertPlatinumLicense(plugins); const soClient = (await context.core).savedObjects.client; const getSLOSuggestions = new GetSLOSuggestions(soClient); - return await getSLOSuggestions.execute(); + return await executeWithErrorHandler(() => getSLOSuggestions.execute()); }, }); @@ -519,13 +509,14 @@ const deleteSloInstancesRoute = createSloServerRoute({ access: 'public', }, params: deleteSLOInstancesParamsSchema, - handler: async ({ context, params }) => { - await assertPlatinumLicense(context); + handler: async ({ response, context, params, plugins }) => { + await assertPlatinumLicense(plugins); const esClient = (await context.core).elasticsearch.client.asCurrentUser; const deleteSloInstances = new DeleteSLOInstances(esClient); - await deleteSloInstances.execute(params.body); + await executeWithErrorHandler(() => deleteSloInstances.execute(params.body)); + return response.noContent(); }, }); @@ -536,16 +527,14 @@ const findSloDefinitionsRoute = createSloServerRoute({ access: 'public', }, params: findSloDefinitionsParamsSchema, - handler: async ({ context, params, logger }) => { - await assertPlatinumLicense(context); + handler: async ({ context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); const soClient = (await context.core).savedObjects.client; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); const findSloDefinitions = new FindSLODefinitions(repository); - const response = await findSloDefinitions.execute(params?.query ?? {}); - - return response; + return await executeWithErrorHandler(() => findSloDefinitions.execute(params?.query ?? {})); }, }); @@ -556,15 +545,13 @@ const fetchHistoricalSummary = createSloServerRoute({ access: 'internal', }, params: fetchHistoricalSummaryParamsSchema, - handler: async ({ context, params, logger }) => { - await assertPlatinumLicense(context); + handler: async ({ context, params, plugins }) => { + await assertPlatinumLicense(plugins); const esClient = (await context.core).elasticsearch.client.asCurrentUser; const historicalSummaryClient = new DefaultHistoricalSummaryClient(esClient); - const historicalSummary = await historicalSummaryClient.fetch(params.body); - - return fetchHistoricalSummaryResponseSchema.encode(historicalSummary); + return await executeWithErrorHandler(() => historicalSummaryClient.fetch(params.body)); }, }); @@ -575,18 +562,15 @@ const getSLOInstancesRoute = createSloServerRoute({ access: 'internal', }, params: getSLOInstancesParamsSchema, - handler: async ({ context, params, logger }) => { - await assertPlatinumLicense(context); + handler: async ({ context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; const repository = new KibanaSavedObjectsSLORepository(soClient, logger); - const getSLOInstances = new GetSLOInstances(repository, esClient); - const response = await getSLOInstances.execute(params.path.id); - - return response; + return await executeWithErrorHandler(() => getSLOInstances.execute(params.path.id)); }, }); @@ -597,9 +581,9 @@ const getDiagnosisRoute = createSloServerRoute({ access: 'internal', }, params: undefined, - handler: async ({ context }) => { + handler: async ({ context, plugins }) => { const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const licensing = await context.licensing; + const licensing = await plugins.licensing.start(); try { const response = await getGlobalDiagnosis(esClient, licensing); @@ -620,8 +604,8 @@ const fetchSloHealthRoute = createSloServerRoute({ access: 'internal', }, params: fetchSLOHealthParamsSchema, - handler: async ({ context, params, logger }) => { - await assertPlatinumLicense(context); + handler: async ({ context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); const core = await context.core; const scopedClusterClient = core.elasticsearch.client; @@ -631,7 +615,7 @@ const fetchSloHealthRoute = createSloServerRoute({ const getSLOHealth = new GetSLOHealth(esClient, scopedClusterClient, repository); - return await getSLOHealth.execute(params.body); + return await executeWithErrorHandler(() => getSLOHealth.execute(params.body)); }, }); @@ -642,28 +626,29 @@ const getSloBurnRates = createSloServerRoute({ access: 'internal', }, params: getSLOBurnRatesParamsSchema, - handler: async ({ request, context, params, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ request, context, params, logger, plugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService.getActiveSpace(request))?.id ?? 'default'; + const spaceId = await getSpaceId(plugins, request); const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; const { instanceId, windows, remoteName } = params.body; - return await getBurnRates({ - instanceId, - spaceId, - windows, - remoteName, - sloId: params.path.id, - services: { - soClient, - esClient, - logger, - }, - }); + return await executeWithErrorHandler(() => + getBurnRates({ + instanceId, + spaceId, + windows, + remoteName, + sloId: params.path.id, + services: { + soClient, + esClient, + logger, + }, + }) + ); }, }); @@ -674,12 +659,11 @@ const getPreviewData = createSloServerRoute({ access: 'internal', }, params: getPreviewDataParamsSchema, - handler: async ({ request, context, params, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ request, context, params, plugins }) => { + await assertPlatinumLicense(plugins); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; - const dataViews = await dependencies.getDataViewsStart(); + const spaceId = await getSpaceId(plugins, request); + const dataViews = await plugins.dataViews.start(); const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; @@ -695,11 +679,12 @@ const getSloSettingsRoute = createSloServerRoute({ tags: ['access:slo_read'], access: 'internal', }, - handler: async ({ context }) => { - await assertPlatinumLicense(context); + handler: async ({ context, plugins }) => { + await assertPlatinumLicense(plugins); const soClient = (await context.core).savedObjects.client; - return await getSloSettings(soClient); + + return await executeWithErrorHandler(() => getSloSettings(soClient)); }, }); @@ -711,11 +696,13 @@ const putSloSettings = (isServerless?: boolean) => access: 'internal', }, params: isServerless ? putSLOServerlessSettingsParamsSchema : putSLOSettingsParamsSchema, - handler: async ({ context, params }) => { - await assertPlatinumLicense(context); + handler: async ({ context, params, plugins }) => { + await assertPlatinumLicense(plugins); const soClient = (await context.core).savedObjects.client; - return await storeSloSettings(soClient, params.body as PutSLOSettingsParams); + return await executeWithErrorHandler(() => + storeSloSettings(soClient, params.body as PutSLOSettingsParams) + ); }, }); @@ -726,17 +713,19 @@ const getSLOsOverview = createSloServerRoute({ access: 'internal', }, params: getOverviewParamsSchema, - handler: async ({ context, params, request, logger, dependencies }) => { - await assertPlatinumLicense(context); + handler: async ({ context, params, request, logger, plugins }) => { + await assertPlatinumLicense(plugins); const soClient = (await context.core).savedObjects.client; const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const racClient = await dependencies.getRacClientWithRequest(request); + const ruleRegistry = await plugins.ruleRegistry.start(); + const racClient = await ruleRegistry.getRacClientWithRequest(request); - const spaces = await dependencies.getSpacesStart(); - const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default'; - const rulesClient = await dependencies.getRulesClientWithRequest(request); + const spaceId = await getSpaceId(plugins, request); + + const alerting = await plugins.alerting.start(); + const rulesClient = await alerting.getRulesClientWithRequest(request); const slosOverview = new GetSLOsOverview( soClient, @@ -746,7 +735,8 @@ const getSLOsOverview = createSloServerRoute({ rulesClient, racClient ); - return await slosOverview.execute(params?.query ?? {}); + + return await executeWithErrorHandler(() => slosOverview.execute(params?.query ?? {})); }, }); diff --git a/x-pack/plugins/observability_solution/slo/server/routes/types.ts b/x-pack/plugins/observability_solution/slo/server/routes/types.ts index a16ffbcc10fa7..cb5057cee4056 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/types.ts @@ -4,32 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { EndpointOf, ReturnOf, ServerRouteRepository } from '@kbn/server-route-repository'; -import { KibanaRequest, Logger } from '@kbn/core/server'; +import { CoreSetup } from '@kbn/core/server'; +import type { DefaultRouteHandlerResources } from '@kbn/server-route-repository'; +import { SLOPluginSetupDependencies, SLOPluginStartDependencies } from '../types'; -import { SloServerRouteRepository } from './get_slo_server_route_repository'; -import { SloRequestHandlerContext } from '../types'; -import { RegisterRoutesDependencies } from './register_routes'; -import { SloConfig } from '..'; - -export type { SloServerRouteRepository }; - -export interface SloRouteHandlerResources { - context: SloRequestHandlerContext; - dependencies: RegisterRoutesDependencies; - logger: Logger; - request: KibanaRequest; - config: SloConfig; -} - -export interface SloRouteCreateOptions { - options: { - tags: string[]; - access?: 'public' | 'internal'; +export interface SLORoutesDependencies { + plugins: { + [key in keyof SLOPluginSetupDependencies]: { + setup: Required[key]; + }; + } & { + [key in keyof SLOPluginStartDependencies]: { + start: () => Promise[key]>; + }; }; + corePlugins: CoreSetup; } -export type AbstractSloServerRouteRepository = ServerRouteRepository; - -export type ObservabilityAPIReturnType> = - ReturnOf; +export type SLORouteHandlerResources = SLORoutesDependencies & DefaultRouteHandlerResources; diff --git a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap index 90690a4989586..ec81df9f08fdd 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSLO resets all associated resources 1`] = ` +exports[`ResetSLO happy path resets all associated resources 1`] = ` [MockFunction] { "calls": Array [ Array [ @@ -16,7 +16,7 @@ exports[`ResetSLO resets all associated resources 1`] = ` } `; -exports[`ResetSLO resets all associated resources 2`] = ` +exports[`ResetSLO happy path resets all associated resources 2`] = ` [MockFunction] { "calls": Array [ Array [ @@ -32,7 +32,7 @@ exports[`ResetSLO resets all associated resources 2`] = ` } `; -exports[`ResetSLO resets all associated resources 3`] = ` +exports[`ResetSLO happy path resets all associated resources 3`] = ` [MockFunction] { "calls": Array [ Array [ @@ -48,7 +48,7 @@ exports[`ResetSLO resets all associated resources 3`] = ` } `; -exports[`ResetSLO resets all associated resources 4`] = ` +exports[`ResetSLO happy path resets all associated resources 4`] = ` [MockFunction] { "calls": Array [ Array [ @@ -64,7 +64,7 @@ exports[`ResetSLO resets all associated resources 4`] = ` } `; -exports[`ResetSLO resets all associated resources 5`] = ` +exports[`ResetSLO happy path resets all associated resources 5`] = ` [MockFunction] { "calls": Array [ Array [ @@ -115,7 +115,7 @@ exports[`ResetSLO resets all associated resources 5`] = ` } `; -exports[`ResetSLO resets all associated resources 6`] = ` +exports[`ResetSLO happy path resets all associated resources 6`] = ` [MockFunction] { "calls": Array [ Array [ @@ -178,7 +178,7 @@ exports[`ResetSLO resets all associated resources 6`] = ` } `; -exports[`ResetSLO resets all associated resources 7`] = ` +exports[`ResetSLO happy path resets all associated resources 7`] = ` [MockFunction] { "calls": Array [ Array [ @@ -194,7 +194,7 @@ exports[`ResetSLO resets all associated resources 7`] = ` } `; -exports[`ResetSLO resets all associated resources 8`] = ` +exports[`ResetSLO happy path resets all associated resources 8`] = ` [MockFunction] { "calls": Array [ Array [ @@ -542,7 +542,7 @@ exports[`ResetSLO resets all associated resources 8`] = ` } `; -exports[`ResetSLO resets all associated resources 9`] = ` +exports[`ResetSLO happy path resets all associated resources 9`] = ` [MockFunction] { "calls": Array [ Array [ @@ -605,7 +605,7 @@ exports[`ResetSLO resets all associated resources 9`] = ` } `; -exports[`ResetSLO resets all associated resources 10`] = ` +exports[`ResetSLO happy path resets all associated resources 10`] = ` [MockFunction] { "calls": Array [ Array [ @@ -621,7 +621,7 @@ exports[`ResetSLO resets all associated resources 10`] = ` } `; -exports[`ResetSLO resets all associated resources 11`] = ` +exports[`ResetSLO happy path resets all associated resources 11`] = ` [MockFunction] { "calls": Array [ Array [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts index 84edf74f18aa5..342b1a4190748 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts @@ -23,6 +23,7 @@ import { } from './mocks'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; +import { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; describe('CreateSLO', () => { let mockEsClient: ElasticsearchClientMock; @@ -55,11 +56,19 @@ describe('CreateSLO', () => { }); describe('happy path', () => { + beforeEach(() => { + mockRepository.exists.mockResolvedValue(false); + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + it('calls the expected services', async () => { const sloParams = createSLOParams({ id: 'unique-id', indicator: createAPMTransactionErrorRateIndicator(), }); + mockTransformManager.install.mockResolvedValue('slo-id-revision'); mockSummaryTransformManager.install.mockResolvedValue('slo-summary-id-revision'); @@ -157,6 +166,33 @@ describe('CreateSLO', () => { }); describe('unhappy path', () => { + beforeEach(() => { + mockRepository.exists.mockResolvedValue(false); + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + + it('throws a SLOIdConflict error when the SLO already exists', async () => { + mockRepository.exists.mockResolvedValue(true); + + const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); + + await expect(createSLO.execute(sloParams)).rejects.toThrowError(/SLO \[.*\] already exists/); + }); + + it('throws a SecurityException error when the user does not have the required privileges', async () => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: false, + } as SecurityHasPrivilegesResponse); + + const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); + + await expect(createSLO.execute(sloParams)).rejects.toThrowError( + "Missing ['read', 'view_index_metadata'] privileges on the source index [metrics-apm*]" + ); + }); + it('rollbacks completed operations when rollup transform install fails', async () => { mockTransformManager.install.mockRejectedValue(new Error('Rollup transform install error')); const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts index 3845ec2ddbd4f..e7c09c352bd66 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts @@ -4,30 +4,30 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { IScopedClusterClient } from '@kbn/core/server'; +import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient, IBasePath, Logger } from '@kbn/core/server'; +import { ElasticsearchClient, IBasePath, IScopedClusterClient, Logger } from '@kbn/core/server'; import { ALL_VALUE, CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema'; import { asyncForEach } from '@kbn/std'; import { v4 as uuidv4 } from 'uuid'; -import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; import { + SLO_MODEL_VERSION, + SLO_SUMMARY_TEMP_INDEX_NAME, getSLOPipelineId, getSLOSummaryPipelineId, getSLOSummaryTransformId, getSLOTransformId, - SLO_MODEL_VERSION, - SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../common/constants'; import { getSLOPipelineTemplate } from '../assets/ingest_templates/slo_pipeline_template'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; import { Duration, DurationUnit, SLODefinition } from '../domain/models'; import { validateSLO } from '../domain/services'; -import { SecurityException, SLOIdConflict } from '../errors'; +import { SLOIdConflict, SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; +import { assertExpectedIndicatorSourceIndexPrivileges } from './utils/assert_expected_indicator_source_index_privileges'; import { getTransformQueryComposite } from './utils/get_transform_compite_query'; export class CreateSLO { @@ -46,16 +46,11 @@ export class CreateSLO { const slo = this.toSLO(params); validateSLO(slo); - const rollbackOperations = []; - - const sloAlreadyExists = await this.repository.checkIfSLOExists(slo); - - if (sloAlreadyExists) { - throw new SLOIdConflict(`SLO [${slo.id}] already exists`); - } + await this.assertSLOInexistant(slo); + await assertExpectedIndicatorSourceIndexPrivileges(slo, this.esClient); + const rollbackOperations = []; const createPromise = this.repository.create(slo); - rollbackOperations.push(() => this.repository.deleteById(slo.id, true)); const rollupTransformId = getSLOTransformId(slo.id, slo.revision); @@ -123,6 +118,12 @@ export class CreateSLO { return this.toResponse(slo); } + private async assertSLOInexistant(slo: SLODefinition) { + const exists = await this.repository.exists(slo.id); + if (exists) { + throw new SLOIdConflict(`SLO [${slo.id}] already exists`); + } + } async createTempSummaryDocument(slo: SLODefinition) { return await retryTransientEsErrors( () => diff --git a/x-pack/plugins/observability_solution/slo/server/services/get_diagnosis.ts b/x-pack/plugins/observability_solution/slo/server/services/get_diagnosis.ts index 7a090de252c1d..df0a7b1952406 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/get_diagnosis.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/get_diagnosis.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/server'; export const MINIMUM_INDEX_PRIVILEGE_SET_EDITOR = [ 'write', @@ -26,9 +26,9 @@ export const TOTAL_INDEX_PRIVILEGE_SET_VIEWER = ['read', 'read_cross_cluster']; export async function getGlobalDiagnosis( esClient: ElasticsearchClient, - licensing: LicensingApiRequestHandlerContext + licensing: LicensingPluginStart ) { - const licenseInfo = licensing.license.toJSON(); + const licenseInfo = (await licensing.getLicense()).toJSON(); const userWritePrivileges = await esClient.security.hasPrivileges({ index: [ { diff --git a/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts b/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts index dc458fcdb813e..ab8230cfec463 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts @@ -48,7 +48,7 @@ const createSLORepositoryMock = (): jest.Mocked => { findAllByIds: jest.fn(), deleteById: jest.fn(), search: jest.fn(), - checkIfSLOExists: jest.fn(), + exists: jest.fn(), }; }; diff --git a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts index 4e66d992b46cd..ab806f221a888 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; +import { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; import { + ElasticsearchClientMock, elasticsearchServiceMock, httpServiceMock, loggingSystemMock, ScopedClusterClientMock, } from '@kbn/core/server/mocks'; import { MockedLogger } from '@kbn/logging-mocks'; - import { SLO_MODEL_VERSION } from '../../common/constants'; import { createSLO } from './fixtures/slo'; import { @@ -31,7 +31,7 @@ describe('ResetSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; let mockSummaryTransformManager: jest.Mocked; - let mockEsClient: jest.Mocked; + let mockEsClient: ElasticsearchClientMock; let mockScopedClusterClient: ScopedClusterClientMock; let loggerMock: jest.Mocked; let resetSLO: ResetSLO; @@ -60,37 +60,62 @@ describe('ResetSLO', () => { jest.useRealTimers(); }); - it('resets all associated resources', async () => { - const slo = createSLO({ id: 'irrelevant', version: 1 }); - mockRepository.findById.mockResolvedValueOnce(slo); - mockRepository.update.mockImplementation((v) => Promise.resolve(v)); + describe('happy path', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + + it('resets all associated resources', async () => { + const slo = createSLO({ id: 'irrelevant', version: 1 }); + mockRepository.findById.mockResolvedValueOnce(slo); + mockRepository.update.mockImplementation((v) => Promise.resolve(v)); + + await resetSLO.execute(slo.id); + + // delete existing resources and data + expect(mockSummaryTransformManager.stop).toMatchSnapshot(); + expect(mockSummaryTransformManager.uninstall).toMatchSnapshot(); - await resetSLO.execute(slo.id); + expect(mockTransformManager.stop).toMatchSnapshot(); + expect(mockTransformManager.uninstall).toMatchSnapshot(); - // delete existing resources and data - expect(mockSummaryTransformManager.stop).toMatchSnapshot(); - expect(mockSummaryTransformManager.uninstall).toMatchSnapshot(); + expect(mockEsClient.deleteByQuery).toMatchSnapshot(); - expect(mockTransformManager.stop).toMatchSnapshot(); - expect(mockTransformManager.uninstall).toMatchSnapshot(); + // install resources + expect(mockSummaryTransformManager.install).toMatchSnapshot(); + expect(mockSummaryTransformManager.start).toMatchSnapshot(); - expect(mockEsClient.deleteByQuery).toMatchSnapshot(); + expect(mockScopedClusterClient.asSecondaryAuthUser.ingest.putPipeline).toMatchSnapshot(); - // install resources - expect(mockSummaryTransformManager.install).toMatchSnapshot(); - expect(mockSummaryTransformManager.start).toMatchSnapshot(); + expect(mockTransformManager.install).toMatchSnapshot(); + expect(mockTransformManager.start).toMatchSnapshot(); - expect(mockScopedClusterClient.asSecondaryAuthUser.ingest.putPipeline).toMatchSnapshot(); + expect(mockEsClient.index).toMatchSnapshot(); - expect(mockTransformManager.install).toMatchSnapshot(); - expect(mockTransformManager.start).toMatchSnapshot(); + expect(mockRepository.update).toHaveBeenCalledWith({ + ...slo, + version: SLO_MODEL_VERSION, + updatedAt: expect.anything(), + }); + }); + }); + + describe('unhappy path', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: false, + } as SecurityHasPrivilegesResponse); + }); - expect(mockEsClient.index).toMatchSnapshot(); + it('throws a SecurityException error when the user does not have the required privileges', async () => { + const slo = createSLO({ id: 'irrelevant', version: 1 }); + mockRepository.findById.mockResolvedValueOnce(slo); - expect(mockRepository.update).toHaveBeenCalledWith({ - ...slo, - version: SLO_MODEL_VERSION, - updatedAt: expect.anything(), + await expect(resetSLO.execute(slo.id)).rejects.toThrowError( + "Missing ['read', 'view_index_metadata'] privileges on the source index [metrics-apm*]" + ); }); }); }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts index 634f02c8f6f90..c9da382c2d6ce 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts @@ -5,17 +5,17 @@ * 2.0. */ -import { ElasticsearchClient, IBasePath, Logger, IScopedClusterClient } from '@kbn/core/server'; +import { ElasticsearchClient, IBasePath, IScopedClusterClient, Logger } from '@kbn/core/server'; import { resetSLOResponseSchema } from '@kbn/slo-schema'; import { - getSLOPipelineId, - getSLOSummaryPipelineId, - getSLOSummaryTransformId, - getSLOTransformId, SLO_DESTINATION_INDEX_PATTERN, SLO_MODEL_VERSION, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, SLO_SUMMARY_TEMP_INDEX_NAME, + getSLOPipelineId, + getSLOSummaryPipelineId, + getSLOSummaryTransformId, + getSLOTransformId, } from '../../common/constants'; import { getSLOPipelineTemplate } from '../assets/ingest_templates/slo_pipeline_template'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; @@ -23,6 +23,7 @@ import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; +import { assertExpectedIndicatorSourceIndexPrivileges } from './utils/assert_expected_indicator_source_index_privileges'; export class ResetSLO { constructor( @@ -39,6 +40,8 @@ export class ResetSLO { public async execute(sloId: string) { const slo = await this.repository.findById(sloId); + await assertExpectedIndicatorSourceIndexPrivileges(slo, this.esClient); + const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); await this.summaryTransformManager.stop(summaryTransformId); await this.summaryTransformManager.uninstall(summaryTransformId); diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts index 243b2b5e9958b..633ee359ca53f 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts @@ -88,7 +88,7 @@ describe('KibanaSavedObjectsSLORepository', () => { soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo)); const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock); - await repository.checkIfSLOExists(slo); + await repository.exists(slo.id); expect(soClientMock.find).toHaveBeenCalledWith({ type: SO_SLO_TYPE, @@ -117,7 +117,7 @@ describe('KibanaSavedObjectsSLORepository', () => { soClientMock.find.mockResolvedValueOnce(soFindResponse([slo])); const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock); - await expect(await repository.checkIfSLOExists(slo)).toEqual(true); + await expect(await repository.exists(slo.id)).toEqual(true); expect(soClientMock.find).toHaveBeenCalledWith({ type: SO_SLO_TYPE, perPage: 0, diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts index 35266ea993bfb..4f9cf439e8ed1 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts @@ -15,7 +15,7 @@ import { SLONotFound } from '../errors'; import { SO_SLO_TYPE } from '../saved_objects'; export interface SLORepository { - checkIfSLOExists(slo: SLODefinition): Promise; + exists(id: string): Promise; create(slo: SLODefinition): Promise; update(slo: SLODefinition): Promise; findAllByIds(ids: string[]): Promise; @@ -31,11 +31,11 @@ export interface SLORepository { export class KibanaSavedObjectsSLORepository implements SLORepository { constructor(private soClient: SavedObjectsClientContract, private logger: Logger) {} - async checkIfSLOExists(slo: SLODefinition) { + async exists(id: string) { const findResponse = await this.soClient.find({ type: SO_SLO_TYPE, perPage: 0, - filter: `slo.attributes.id:(${slo.id})`, + filter: `slo.attributes.id:(${id})`, }); return findResponse.total > 0; diff --git a/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.test.ts b/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.test.ts index b947e0985c580..31b74b3cf9ed4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.test.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { getDeleteQueryFilter, SloOrphanSummaryCleanupTask } from './orphan_summary_cleanup_task'; -import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; - -import { loggerMock } from '@kbn/logging-mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; -import { times } from 'lodash'; import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { times } from 'lodash'; import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/constants'; +import { getDeleteQueryFilter, SloOrphanSummaryCleanupTask } from './orphan_summary_cleanup_task'; const taskManagerSetup = taskManagerMock.createSetup(); const taskManagerStart = taskManagerMock.createStart(); diff --git a/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts b/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts index bdbb955050358..c3d56854f4946 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/tasks/orphan_summary_cleanup_task.ts @@ -15,8 +15,8 @@ import { AggregationsCompositeAggregateKey } from '@elastic/elasticsearch/lib/ap import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants'; import { StoredSLODefinition } from '../../domain/models'; import { SO_SLO_TYPE } from '../../saved_objects'; -import { SloConfig } from '../..'; import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/constants'; +import { SLOConfig } from '../../types'; export const TASK_TYPE = 'SLO:ORPHAN_SUMMARIES-CLEANUP-TASK'; @@ -49,9 +49,9 @@ export class SloOrphanSummaryCleanupTask { private taskManager?: TaskManagerStartContract; private soClient?: SavedObjectsClientContract; private esClient?: ElasticsearchClient; - private config: SloConfig; + private config: SLOConfig; - constructor(taskManager: TaskManagerSetupContract, logger: Logger, config: SloConfig) { + constructor(taskManager: TaskManagerSetupContract, logger: Logger, config: SLOConfig) { this.logger = logger; this.config = config; diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts index dccfe5f97d633..9417e4779a5e2 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; import { + ElasticsearchClientMock, elasticsearchServiceMock, httpServiceMock, loggingSystemMock, @@ -16,6 +16,7 @@ import { MockedLogger } from '@kbn/logging-mocks'; import { UpdateSLOParams } from '@kbn/slo-schema'; import { cloneDeep, omit, pick } from 'lodash'; +import { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; import { getSLOSummaryTransformId, getSLOTransformId, @@ -42,7 +43,7 @@ import { UpdateSLO } from './update_slo'; describe('UpdateSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; - let mockEsClient: jest.Mocked; + let mockEsClient: ElasticsearchClientMock; let mockScopedClusterClient: ScopedClusterClientMock; let mockLogger: jest.Mocked; let mockSummaryTransformManager: jest.Mocked; @@ -69,6 +70,8 @@ describe('UpdateSLO', () => { describe('when the update payload does not change the original SLO', () => { function expectNoCallsToAnyMocks() { + expect(mockEsClient.security.hasPrivileges).not.toBeCalled(); + expect(mockTransformManager.stop).not.toBeCalled(); expect(mockTransformManager.uninstall).not.toBeCalled(); expect(mockTransformManager.install).not.toBeCalled(); @@ -192,6 +195,12 @@ describe('UpdateSLO', () => { }); describe('handles breaking changes', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + it('consideres a settings change as a breaking change', async () => { const slo = createSLO(); mockRepository.findById.mockResolvedValueOnce(slo); @@ -302,6 +311,32 @@ describe('UpdateSLO', () => { }); describe('when error happens during the update', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + + it('throws a SecurityException error when the user does not have the required privileges on the source index', async () => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: false, + } as SecurityHasPrivilegesResponse); + + const originalSlo = createSLO({ + id: 'original-id', + indicator: createAPMTransactionErrorRateIndicator(), + }); + mockRepository.findById.mockResolvedValueOnce(originalSlo); + + const newIndicator = createAPMTransactionErrorRateIndicator({ index: 'new-index-*' }); + + await expect( + updateSLO.execute(originalSlo.id, { indicator: newIndicator }) + ).rejects.toThrowError( + "Missing ['read', 'view_index_metadata'] privileges on the source index [new-index-*]" + ); + }); + it('restores the previous SLO definition when updated summary transform install fails', async () => { const originalSlo = createSLO({ id: 'original-id', diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts index 9418bfb1ea91a..d1dfb2e70e00c 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { ElasticsearchClient, IBasePath, Logger, IScopedClusterClient } from '@kbn/core/server'; +import { ElasticsearchClient, IBasePath, IScopedClusterClient, Logger } from '@kbn/core/server'; import { UpdateSLOParams, UpdateSLOResponse, updateSLOResponseSchema } from '@kbn/slo-schema'; import { asyncForEach } from '@kbn/std'; import { isEqual, pick } from 'lodash'; import { + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_TEMP_INDEX_NAME, getSLOPipelineId, getSLOSummaryPipelineId, getSLOSummaryTransformId, getSLOTransformId, - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../common/constants'; import { getSLOPipelineTemplate } from '../assets/ingest_templates/slo_pipeline_template'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; @@ -27,6 +27,7 @@ import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; +import { assertExpectedIndicatorSourceIndexPrivileges } from './utils/assert_expected_indicator_source_index_privileges'; export class UpdateSLO { constructor( @@ -68,8 +69,9 @@ export class UpdateSLO { validateSLO(updatedSlo); - const rollbackOperations = []; + await assertExpectedIndicatorSourceIndexPrivileges(updatedSlo, this.esClient); + const rollbackOperations = []; await this.repository.update(updatedSlo); rollbackOperations.push(() => this.repository.update(originalSlo)); diff --git a/x-pack/plugins/observability_solution/slo/server/services/utils/assert_expected_indicator_source_index_privileges.ts b/x-pack/plugins/observability_solution/slo/server/services/utils/assert_expected_indicator_source_index_privileges.ts new file mode 100644 index 0000000000000..d3633865eafb8 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/utils/assert_expected_indicator_source_index_privileges.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core/server'; +import { SLODefinition } from '../../domain/models'; +import { SecurityException } from '../../errors'; + +export async function assertExpectedIndicatorSourceIndexPrivileges( + slo: SLODefinition, + esClient: ElasticsearchClient +) { + const privileges = await esClient.security.hasPrivileges({ + index: [{ names: slo.indicator.params.index, privileges: ['read', 'view_index_metadata'] }], + }); + if (!privileges.has_all_requested) { + throw new SecurityException( + `Missing ['read', 'view_index_metadata'] privileges on the source index [${slo.indicator.params.index}]` + ); + } +} diff --git a/x-pack/plugins/observability_solution/slo/server/types.ts b/x-pack/plugins/observability_solution/slo/server/types.ts index 86bf0ac0b94ab..b9269f49c4d9f 100644 --- a/x-pack/plugins/observability_solution/slo/server/types.ts +++ b/x-pack/plugins/observability_solution/slo/server/types.ts @@ -5,15 +5,49 @@ * 2.0. */ -import type { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; -import type { AlertingApiRequestHandlerContext } from '@kbn/alerting-plugin/server'; -import type { CustomRequestHandlerContext, CoreRequestHandlerContext } from '@kbn/core/server'; +import type { PluginSetupContract, PluginStartContract } from '@kbn/alerting-plugin/server'; +import { CloudSetup } from '@kbn/cloud-plugin/server'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import { + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, +} from '@kbn/rule-registry-plugin/server'; +import { SharePluginSetup } from '@kbn/share-plugin/server'; +import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; -/** - * @internal - */ -export type SloRequestHandlerContext = CustomRequestHandlerContext<{ - licensing: LicensingApiRequestHandlerContext; - alerting: AlertingApiRequestHandlerContext; - core: Promise; -}>; +export type { SLOConfig } from '../common/config'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SLOServerSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SLOServerStart {} + +export interface SLOPluginSetupDependencies { + alerting: PluginSetupContract; + ruleRegistry: RuleRegistryPluginSetupContract; + share: SharePluginSetup; + features: FeaturesPluginSetup; + taskManager: TaskManagerSetupContract; + spaces: SpacesPluginSetup; + cloud?: CloudSetup; + usageCollection: UsageCollectionSetup; + licensing: LicensingPluginSetup; + dataViews: DataViewsServerPluginStart; +} + +export interface SLOPluginStartDependencies { + alerting: PluginStartContract; + taskManager: TaskManagerStartContract; + spaces?: SpacesPluginStart; + ruleRegistry: RuleRegistryPluginStartContract; + dataViews: DataViewsServerPluginStart; + licensing: LicensingPluginStart; +} diff --git a/x-pack/plugins/observability_solution/slo/tsconfig.json b/x-pack/plugins/observability_solution/slo/tsconfig.json index 2bf0737b5436c..4b05b5aa0b063 100644 --- a/x-pack/plugins/observability_solution/slo/tsconfig.json +++ b/x-pack/plugins/observability_solution/slo/tsconfig.json @@ -95,6 +95,8 @@ "@kbn/core-application-browser", "@kbn/core-theme-browser", "@kbn/ebt-tools", - "@kbn/observability-alerting-rule-utils" + "@kbn/observability-alerting-rule-utils", + "@kbn/server-route-repository-client", + "@kbn/server-route-repository-utils" ] } diff --git a/x-pack/plugins/observability_solution/synthetics/public/plugin.ts b/x-pack/plugins/observability_solution/synthetics/public/plugin.ts index 15de2de6d5eee..e8fbedfa0ecb1 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/plugin.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/plugin.ts @@ -60,7 +60,7 @@ import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/pu import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import { DashboardStart, DashboardSetup } from '@kbn/dashboard-plugin/public'; -import { SloPublicStart } from '@kbn/slo-plugin/public'; +import { SLOPublicStart } from '@kbn/slo-plugin/public'; import { registerSyntheticsEmbeddables } from './apps/embeddables/register_embeddables'; import { kibanaService } from './utils/kibana_service'; import { PLUGIN } from '../common/constants/plugin'; @@ -111,7 +111,7 @@ export interface ClientPluginsStart { usageCollection: UsageCollectionStart; serverless: ServerlessPluginStart; licenseManagement?: LicenseManagementUIPluginSetup; - slo?: SloPublicStart; + slo?: SLOPublicStart; presentationUtil: PresentationUtilPluginStart; dashboard: DashboardStart; } diff --git a/x-pack/plugins/search_playground/public/components/summarization_panel/instructions_field.tsx b/x-pack/plugins/search_playground/public/components/summarization_panel/instructions_field.tsx index 0bf870202f1e9..fe51adcd6ead1 100644 --- a/x-pack/plugins/search_playground/public/components/summarization_panel/instructions_field.tsx +++ b/x-pack/plugins/search_playground/public/components/summarization_panel/instructions_field.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { EuiFormRow, EuiIcon, EuiTextArea, EuiToolTip } from '@elastic/eui'; +import { EuiFormRow, EuiTextArea, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; @@ -37,21 +37,20 @@ export const InstructionsField: React.FC = ({ value, onC return ( + {i18n.translate('xpack.searchPlayground.sidebar.instructionsField.label', { + defaultMessage: 'Instructions', })} - > - <> - - {i18n.translate('xpack.searchPlayground.sidebar.instructionsField.label', { - defaultMessage: 'Instructions', - })} - - - - +   + + } fullWidth > diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts index 1bf3bfb5e2445..2d722e7d5076a 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts @@ -555,10 +555,15 @@ export const RuleExceptionList = z.object({ namespace_type: z.enum(['agnostic', 'single']), }); +export type AlertSuppressionDurationUnit = z.infer; +export const AlertSuppressionDurationUnit = z.enum(['s', 'm', 'h']); +export type AlertSuppressionDurationUnitEnum = typeof AlertSuppressionDurationUnit.enum; +export const AlertSuppressionDurationUnitEnum = AlertSuppressionDurationUnit.enum; + export type AlertSuppressionDuration = z.infer; export const AlertSuppressionDuration = z.object({ value: z.number().int().min(1), - unit: z.enum(['s', 'm', 'h']), + unit: AlertSuppressionDurationUnit, }); /** diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml index 088153cca4885..029a71b9e0a1c 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml @@ -581,6 +581,13 @@ components: - type - namespace_type + AlertSuppressionDurationUnit: + type: string + enum: + - s + - m + - h + AlertSuppressionDuration: type: object properties: @@ -588,11 +595,7 @@ components: type: integer minimum: 1 unit: - type: string - enum: - - s - - m - - h + $ref: '#/components/schemas/AlertSuppressionDurationUnit' required: - value - unit diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 892b0a0226639..dc6495e1d9737 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -108,11 +108,6 @@ export const allowedExperimentalValues = Object.freeze({ */ assistantModelEvaluation: false, - /** - * Enables new Knowledge Base Entries features, introduced in `8.15.0`. - */ - assistantKnowledgeBaseByDefault: true, - /** * Enables the Managed User section inside the new user details flyout. */ diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index ebd4c93280090..7e8d7a61bff2c 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -1560,17 +1560,19 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: '#/components/schemas/AlertSuppressionDurationUnit' value: minimum: 1 type: integer required: - value - unit + AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string AlertSuppressionGroupBy: items: type: string diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 30a7ae3ea343e..58456e71140a0 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -850,17 +850,19 @@ components: type: object properties: unit: - enum: - - s - - m - - h - type: string + $ref: '#/components/schemas/AlertSuppressionDurationUnit' value: minimum: 1 type: integer required: - value - unit + AlertSuppressionDurationUnit: + enum: + - s + - m + - h + type: string AlertSuppressionGroupBy: items: type: string diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx index 8d4088b19f9b6..69912c58e4e15 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx @@ -126,11 +126,14 @@ export const MisconfigurationFindingsDetailsTable = memo( return getNavUrlParams({ [queryField]: name }, 'configurations', ['rule.name']); }; + const linkWidth = 40; + const resultWidth = 74; + const columns: Array> = [ { field: 'rule', name: '', - width: '5%', + width: `${linkWidth}`, render: (rule: CspBenchmarkRuleMetadata, finding: MisconfigurationFindingDetailFields) => ( { const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name); const insightPluginWithLicense = insightMarkdownPlugin.plugin({ diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx index 37d4e004edf54..026e070f320df 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx @@ -135,7 +135,7 @@ describe('plugin', () => { }); it('show investigate message when insightsUpsellingMessage is not provided', () => { - const result = plugin({ insightsUpsellingMessage: null }); + const result = plugin({ insightsUpsellingMessage: undefined }); expect(result.button.label).toEqual('Investigate'); }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index e8fa43d0a189e..794b17a219208 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -541,11 +541,7 @@ const exampleInsight = `${insightPrefix}{ ] }}`; -export const plugin = ({ - insightsUpsellingMessage, -}: { - insightsUpsellingMessage: string | null; -}) => { +export const plugin = ({ insightsUpsellingMessage }: { insightsUpsellingMessage?: string }) => { return { name: 'insights', button: { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx index 6a37280f9ef23..67b3f9e2b4f8a 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/plugin.tsx @@ -161,7 +161,7 @@ const OsqueryEditor = React.memo(OsqueryEditorComponent); export const plugin = ({ interactionsUpsellingMessage, }: { - interactionsUpsellingMessage: string | null; + interactionsUpsellingMessage?: string; }) => { return { name: 'osquery', diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx index 40b2fba4b84d0..4d5b2e14e0d95 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx @@ -78,7 +78,7 @@ const TimelineEditor = memo(TimelineEditorComponent); export const plugin = ({ interactionsUpsellingMessage, }: { - interactionsUpsellingMessage: string | null; + interactionsUpsellingMessage?: string; }): EuiMarkdownEditorUiPlugin => { return { name: ID, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx index ee783bcd19e3d..c18a6282eb373 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx @@ -71,7 +71,7 @@ describe('use_upselling', () => { expect(result.all.length).toBe(1); // assert that it should not cause unnecessary re-renders }); - test('useUpsellingMessage returns null when upsellingMessageId not found', () => { + test('useUpsellingMessage returns undefined when upsellingMessageId not found', () => { const emptyMessages = {}; mockUpselling.setPages(emptyMessages); @@ -81,6 +81,6 @@ describe('use_upselling', () => { wrapper: RenderWrapper, } ); - expect(result.current).toBe(null); + expect(result.current).toBeUndefined(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts index 8ef01b5b56a25..9f452bb4f2872 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.ts @@ -22,11 +22,11 @@ export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentTy return useMemo(() => upsellingSections?.get(id) ?? null, [id, upsellingSections]); }; -export const useUpsellingMessage = (id: UpsellingMessageId): string | null => { +export const useUpsellingMessage = (id: UpsellingMessageId): string | undefined => { const upselling = useUpsellingService(); const upsellingMessages = useObservable(upselling.messages$, upselling.getMessagesValue()); - return useMemo(() => upsellingMessages?.get(id) ?? null, [id, upsellingMessages]); + return useMemo(() => upsellingMessages?.get(id), [id, upsellingMessages]); }; export const useUpsellingPage = (pageName: SecurityPageName): React.ComponentType | null => { diff --git a/x-pack/plugins/security_solution/public/common/test/eui/combobox.ts b/x-pack/plugins/security_solution/public/common/test/eui/combobox.ts new file mode 100644 index 0000000000000..dad99a3c426d4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/test/eui/combobox.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, fireEvent, waitFor } from '@testing-library/react'; + +export function showEuiComboBoxOptions(comboBoxToggleButton: HTMLElement): Promise { + fireEvent.click(comboBoxToggleButton); + + return waitFor(() => { + const listWithOptionsElement = document.querySelector('[role="listbox"]'); + const emptyListElement = document.querySelector('.euiComboBoxOptionsList__empty'); + + expect(listWithOptionsElement || emptyListElement).toBeInTheDocument(); + }); +} + +type SelectEuiComboBoxOptionParameters = + | { + comboBoxToggleButton: HTMLElement; + optionIndex: number; + optionText?: undefined; + } + | { + comboBoxToggleButton: HTMLElement; + optionText: string; + optionIndex?: undefined; + }; + +export function selectEuiComboBoxOption({ + comboBoxToggleButton, + optionIndex, + optionText, +}: SelectEuiComboBoxOptionParameters): Promise { + return act(async () => { + await showEuiComboBoxOptions(comboBoxToggleButton); + + const options = Array.from( + document.querySelectorAll('[data-test-subj*="comboBoxOptionsList"] [role="option"]') + ); + + if (typeof optionText === 'string') { + const optionToSelect = options.find((option) => option.textContent === optionText); + + if (optionToSelect) { + fireEvent.click(optionToSelect); + } else { + throw new Error( + `Could not find option with text "${optionText}". Available options: ${options + .map((option) => option.textContent) + .join(', ')}` + ); + } + } else { + fireEvent.click(options[optionIndex]); + } + }); +} + +export function selectFirstEuiComboBoxOption({ + comboBoxToggleButton, +}: { + comboBoxToggleButton: HTMLElement; +}): Promise { + return selectEuiComboBoxOption({ comboBoxToggleButton, optionIndex: 0 }); +} + +export function clearEuiComboBoxSelection({ + clearButton, +}: { + clearButton: HTMLElement; +}): Promise { + return act(async () => { + fireEvent.click(clearButton); + }); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx new file mode 100644 index 0000000000000..5c6099529e920 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiPanel, EuiText, EuiToolTip } from '@elastic/eui'; +import type { DataViewFieldBase } from '@kbn/es-query'; +import { useFormData } from '../../../../../shared_imports'; +import { MissingFieldsStrategySelector } from './missing_fields_strategy_selector'; +import { SuppressionDurationSelector } from './suppression_duration_selector'; +import { SuppressionFieldsSelector } from './suppression_fields_selector'; +import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../constants/fields'; + +interface AlertSuppressionEditProps { + suppressibleFields: DataViewFieldBase[]; + labelAppend?: React.ReactNode; + disabled?: boolean; + disabledText?: string; + warningText?: string; +} + +export const AlertSuppressionEdit = memo(function AlertSuppressionEdit({ + suppressibleFields, + labelAppend, + disabled, + disabledText, + warningText, +}: AlertSuppressionEditProps): JSX.Element { + const [{ [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: suppressionFields }] = useFormData<{ + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: string[]; + }>({ + watch: ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + }); + const hasSelectedFields = suppressionFields?.length > 0; + const content = ( + <> + + {warningText && ( + + {warningText} + + )} + + + + + + ); + + return disabled && disabledText ? ( + + {content} + + ) : ( + content + ); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/missing_fields_strategy_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/missing_fields_strategy_selector.tsx new file mode 100644 index 0000000000000..a7b38843a61ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/missing_fields_strategy_selector.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import type { EuiFormRowProps, EuiRadioGroupOption, EuiRadioGroupProps } from '@elastic/eui'; +import { RadioGroupField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../../common/api/detection_engine'; +import { UseField } from '../../../../../shared_imports'; +import { SuppressionInfoIcon } from './suppression_info_icon'; +import { ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME } from '../constants/fields'; +import * as i18n from './translations'; + +interface MissingFieldsStrategySelectorProps { + disabled?: boolean; +} + +export function MissingFieldsStrategySelector({ + disabled, +}: MissingFieldsStrategySelectorProps): JSX.Element { + const radioFieldProps: Partial = useMemo( + () => ({ + options: ALERT_SUPPRESSION_MISSING_FIELDS_STRATEGY_OPTIONS, + 'data-test-subj': 'suppressionMissingFieldsOptions', + disabled, + }), + [disabled] + ); + + return ( + + ); +} + +const EUI_FORM_ROW_PROPS: Partial = { + label: ( + + {i18n.ALERT_SUPPRESSION_MISSING_FIELDS_LABEL} + + ), + 'data-test-subj': 'alertSuppressionMissingFields', +}; + +const ALERT_SUPPRESSION_MISSING_FIELDS_STRATEGY_OPTIONS: EuiRadioGroupOption[] = [ + { + id: AlertSuppressionMissingFieldsStrategyEnum.suppress, + label: i18n.ALERT_SUPPRESSION_MISSING_FIELDS_SUPPRESS_OPTION, + }, + { + id: AlertSuppressionMissingFieldsStrategyEnum.doNotSuppress, + label: i18n.ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS_OPTION, + }, +]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_duration_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_duration_selector.tsx new file mode 100644 index 0000000000000..7cf5eeb3018b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_duration_selector.tsx @@ -0,0 +1,140 @@ +/* + * 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, { memo, useEffect } from 'react'; +import { EuiFormRow, EuiRadioGroup, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { FieldHook } from '../../../../../shared_imports'; +import { UseMultiFields } from '../../../../../shared_imports'; +import { AlertSuppressionDurationType } from '../../../../../detections/pages/detection_engine/rules/types'; +import { DurationInput } from '../../duration_input'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, +} from '../constants/fields'; +import * as i18n from './translations'; + +interface AlertSuppressionDurationProps { + onlyPerTimePeriod?: boolean; + onlyPerTimePeriodReasonMessage?: string; + disabled?: boolean; +} + +export function SuppressionDurationSelector({ + onlyPerTimePeriod, + onlyPerTimePeriodReasonMessage, + disabled, +}: AlertSuppressionDurationProps): JSX.Element { + return ( + + + fields={{ + suppressionDurationSelector: { + path: ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + }, + suppressionDurationValue: { + path: `${ALERT_SUPPRESSION_DURATION_FIELD_NAME}.${ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME}`, + }, + suppressionDurationUnit: { + path: `${ALERT_SUPPRESSION_DURATION_FIELD_NAME}.${ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME}`, + }, + }} + > + {({ suppressionDurationSelector, suppressionDurationValue, suppressionDurationUnit }) => ( + + )} + + + ); +} + +interface SuppressionDurationSelectorFieldsProps { + suppressionDurationSelectorField: FieldHook; + suppressionDurationValueField: FieldHook; + suppressionDurationUnitField: FieldHook; + onlyPerTimePeriod?: boolean; + onlyPerTimePeriodReasonMessage?: string; + disabled?: boolean; +} + +const SuppressionDurationSelectorFields = memo(function SuppressionDurationSelectorFields({ + suppressionDurationSelectorField, + suppressionDurationValueField, + suppressionDurationUnitField, + onlyPerTimePeriod = false, + onlyPerTimePeriodReasonMessage, + disabled, +}: SuppressionDurationSelectorFieldsProps): JSX.Element { + const { euiTheme } = useEuiTheme(); + const { value: durationType, setValue: setDurationType } = suppressionDurationSelectorField; + + useEffect(() => { + if (onlyPerTimePeriod && durationType !== AlertSuppressionDurationType.PerTimePeriod) { + setDurationType(AlertSuppressionDurationType.PerTimePeriod); + } + }, [onlyPerTimePeriod, durationType, setDurationType]); + + return ( + <> + + <> {i18n.ALERT_SUPPRESSION_DURATION_PER_RULE_EXECUTION_OPTION} + + ) : ( + i18n.ALERT_SUPPRESSION_DURATION_PER_RULE_EXECUTION_OPTION + ), + disabled: onlyPerTimePeriod ? true : disabled, + }, + { + id: AlertSuppressionDurationType.PerTimePeriod, + disabled, + label: i18n.ALERT_SUPPRESSION_DURATION_PER_TIME_PERIOD_OPTION, + }, + ]} + onChange={(id) => { + suppressionDurationSelectorField.setValue(id); + }} + data-test-subj="alertSuppressionDurationOptions" + /> +
+ +
+ + ); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx new file mode 100644 index 0000000000000..72eea027288f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiFormRow } from '@elastic/eui'; +import type { DataViewFieldBase } from '@kbn/es-query'; +import { UseField } from '../../../../../shared_imports'; +import { MultiSelectFieldsAutocomplete } from '../../../../rule_creation_ui/components/multi_select_fields'; +import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../constants/fields'; +import * as i18n from './translations'; + +interface SuppressionFieldsSelectorProps { + suppressibleFields: DataViewFieldBase[]; + labelAppend?: React.ReactNode; + disabled?: boolean; +} + +export function SuppressionFieldsSelector({ + suppressibleFields, + labelAppend, + disabled, +}: SuppressionFieldsSelectorProps): JSX.Element { + return ( + + <> + + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/suppression_info_icon/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_info_icon.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/suppression_info_icon/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_info_icon.tsx index bb3b0db1ccdab..466600dd394da 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/suppression_info_icon/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_info_icon.tsx @@ -5,28 +5,25 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { EuiLink, EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { useBoolean } from '@kbn/react-hooks'; import { FormattedMessage } from '@kbn/i18n-react'; - -import { useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../../common/lib/kibana'; const POPOVER_WIDTH = 320; /** * Icon and popover that gives hint to users how suppression for missing fields work */ -const SuppressionInfoIconComponent = () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); +export function SuppressionInfoIcon(): JSX.Element { + const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); const { docLinks } = useKibana().services; - const onButtonClick = () => setIsPopoverOpen(!isPopoverOpen); - const closePopover = () => setIsPopoverOpen(false); - const button = ( ); @@ -59,8 +56,4 @@ const SuppressionInfoIconComponent = () => { ); -}; - -export const SuppressionInfoIcon = React.memo(SuppressionInfoIconComponent); - -SuppressionInfoIcon.displayName = 'SuppressionInfoIcon'; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/translations.ts new file mode 100644 index 0000000000000..8da7d435adfeb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/translations.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALERT_SUPPRESSION_SUPPRESS_BY_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.fieldsSelector.label', + { + defaultMessage: 'Suppress alerts by', + } +); + +export const ALERT_SUPPRESSION_SUPPRESS_BY_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.suppressByFields.helpText', + { + defaultMessage: 'Select field(s) to use for suppressing extra alerts', + } +); + +export const ALERT_SUPPRESSION_DURATION_PER_RULE_EXECUTION_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.suppressionDuration.perRuleExecutionOption', + { + defaultMessage: 'Per rule execution', + } +); + +export const ALERT_SUPPRESSION_DURATION_PER_TIME_PERIOD_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.suppressionDuration.perTimePeriodOption', + { + defaultMessage: 'Per time period', + } +); + +export const ALERT_SUPPRESSION_MISSING_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.missingFields.label', + { + defaultMessage: 'If a suppression field is missing', + } +); + +export const ALERT_SUPPRESSION_MISSING_FIELDS_SUPPRESS_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.missingFields.suppressOption', + { + defaultMessage: 'Suppress and group alerts for events with missing fields', + } +); + +export const ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS_OPTION = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.missingFields.doNotSuppressOption', + { + defaultMessage: 'Do not suppress alerts for events with missing fields', + } +); + +export const ALERT_SUPPRESSION_NOT_SUPPORTED_FOR_EQL_SEQUENCE = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.notSupportedForEqlSequence', + { + defaultMessage: 'Suppression is not supported for EQL sequence queries', + } +); + +export const MACHINE_LEARNING_SUPPRESSION_FIELDS_LOADING = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.machineLearningSuppressionFieldsLoading', + { + defaultMessage: 'Machine Learning suppression fields are loading', + } +); + +export const MACHINE_LEARNING_NO_SUPPRESSION_FIELDS = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.machineLearningNoSuppressionFields', + { + defaultMessage: + 'Unable to load machine Learning suppression fields, start relevant Machine Learning jobs.', + } +); + +export const ESQL_SUPPRESSION_FIELDS_LOADING = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.esqlFieldsLoading', + { + defaultMessage: 'ES|QL suppression fields are loading', + } +); + +export const MACHINE_LEARNING_SUPPRESSION_INCOMPLETE_LABEL = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.alertSuppression.machineLearningSuppressionIncomplete', + { + defaultMessage: + 'This list of fields might be incomplete as some Machine Learning jobs are not running. Start all relevant jobs for a complete list.', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/default_duration.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/default_duration.ts new file mode 100644 index 0000000000000..6e06d0d67031a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/default_duration.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertSuppressionDurationUnitEnum } from '../../../../../../common/api/detection_engine'; +import { + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, +} from './fields'; + +export const ALERT_SUPPRESSION_DEFAULT_DURATION = { + [ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME]: 5, + [ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME]: AlertSuppressionDurationUnitEnum.m, +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/fields.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/fields.ts new file mode 100644 index 0000000000000..42a0583e90512 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/constants/fields.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 const ALERT_SUPPRESSION_FIELDS_FIELD_NAME = 'alertSuppressionFields' as const; +export const ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME = 'alertSuppressionDurationType' as const; +export const ALERT_SUPPRESSION_DURATION_FIELD_NAME = 'alertSuppressionDuration' as const; +export const ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME = 'value' as const; +export const ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME = 'unit' as const; +export const ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME = 'alertSuppressionMissingFields' as const; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/index.ts new file mode 100644 index 0000000000000..a97e74726e3c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './components/alert_suppression_edit'; +export * from './components/suppression_duration_selector'; +export * from './constants/fields'; +export * from './constants/default_duration'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/test_helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/test_helpers.ts new file mode 100644 index 0000000000000..b7d6c4003e934 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/test_helpers.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { act, fireEvent, waitFor, within, screen } from '@testing-library/react'; +import type { AlertSuppressionDurationUnit } from '../../../../../common/api/detection_engine'; +import { selectEuiComboBoxOption } from '../../../../common/test/eui/combobox'; + +const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; + +export async function setSuppressionFields(fieldNames: string[]): Promise { + const getAlertSuppressionFieldsComboBoxToggleButton = () => + within(screen.getByTestId('alertSuppressionInput')).getByTestId( + COMBO_BOX_TOGGLE_BUTTON_TEST_ID + ); + + await waitFor(() => { + expect(getAlertSuppressionFieldsComboBoxToggleButton()).toBeInTheDocument(); + }); + + for (const fieldName of fieldNames) { + await selectEuiComboBoxOption({ + comboBoxToggleButton: getAlertSuppressionFieldsComboBoxToggleButton(), + optionText: fieldName, + }); + } +} + +export function expectSuppressionFields(fieldNames: string[]): void { + for (const fieldName of fieldNames) { + expect( + within(screen.getByTestId('alertSuppressionInput')).getByTitle(fieldName) + ).toBeInTheDocument(); + } +} + +export function setDurationType(value: 'Per rule execution' | 'Per time period'): void { + act(() => { + fireEvent.click(within(screen.getByTestId('alertSuppressionDuration')).getByLabelText(value)); + }); +} + +export function setDuration(value: number, unit: AlertSuppressionDurationUnit): void { + act(() => { + fireEvent.input( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('interval'), + { + target: { value: value.toString() }, + } + ); + + fireEvent.change( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('timeType'), + { + target: { value: unit }, + } + ); + }); +} + +export function expectDuration(value: number, unit: AlertSuppressionDurationUnit): void { + expect( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('interval') + ).toHaveValue(value); + expect( + within(screen.getByTestId('alertSuppressionDuration')).getByTestId('timeType') + ).toHaveValue(unit); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/index.tsx similarity index 68% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/index.tsx index 99222756bcf26..b203cdea8f737 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/index.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiFieldNumber, EuiFormRow, EuiSelect, transparentize } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import styled from 'styled-components'; - +import React, { memo, useCallback } from 'react'; +import { css } from '@emotion/css'; +import { EuiFieldNumber, EuiFormRow, EuiSelect, transparentize, useEuiTheme } from '@elastic/eui'; import type { FieldHook } from '../../../../shared_imports'; import { getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as I18n from './translations'; @@ -21,39 +20,10 @@ interface DurationInputProps { durationUnitOptions?: Array<{ value: 's' | 'm' | 'h' | 'd'; text: string }>; } -const getNumberFromUserInput = (input: string, minimumValue = 0): number | undefined => { - const number = parseInt(input, 10); - if (Number.isNaN(number)) { - return minimumValue; - } else { - return Math.max(minimumValue, Math.min(number, Number.MAX_SAFE_INTEGER)); - } -}; - -const StyledEuiFormRow = styled(EuiFormRow)` - max-width: 235px; - - .euiFormControlLayout__append { - padding-inline: 0 !important; - } - - .euiFormControlLayoutIcons { - color: ${({ theme }) => theme.eui.euiColorPrimary}; - } -`; - -const MyEuiSelect = styled(EuiSelect)` - min-width: 106px; // Preserve layout when disabled & dropdown arrow is not rendered - box-shadow: none; - background: ${({ theme }) => - transparentize(theme.eui.euiColorPrimary, 0.1)} !important; // Override focus states etc. - color: ${({ theme }) => theme.eui.euiColorPrimary}; -`; - // This component is similar to the ScheduleItem component, but instead of combining the value // and unit into a single string it keeps them separate. This makes the component simpler and // allows for easier validation of values and units in APIs as well. -const DurationInputComponent: React.FC = ({ +export const DurationInput = memo(function DurationInputComponent({ durationValueField, durationUnitField, minimumValue = 0, @@ -64,7 +34,8 @@ const DurationInputComponent: React.FC = ({ { value: 'h', text: I18n.HOURS }, ], ...props -}: DurationInputProps) => { +}: DurationInputProps): JSX.Element { + const { euiTheme } = useEuiTheme(); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(durationValueField); const { value: durationValue, setValue: setDurationValue } = durationValueField; const { value: durationUnit, setValue: setDurationUnit } = durationUnitField; @@ -84,17 +55,40 @@ const DurationInputComponent: React.FC = ({ [minimumValue, setDurationValue] ); + const durationFormRowStyle = css` + max-width: 235px; + + .euiFormControlLayout__append { + padding-inline: 0 !important; + } + + .euiFormControlLayoutIcons { + color: ${euiTheme.colors.primary}; + } + `; + const durationUnitSelectStyle = css` + min-width: 106px; // Preserve layout when disabled & dropdown arrow is not rendered + box-shadow: none; + background: ${transparentize( + euiTheme.colors.primary, + 0.1 + )} !important; // Override focus states etc. + color: ${euiTheme.colors.primary}; + `; + // EUI missing some props const rest = { disabled: isDisabled, ...props }; return ( - + @@ -106,8 +100,16 @@ const DurationInputComponent: React.FC = ({ data-test-subj="interval" {...rest} /> - +
); -}; +}); + +function getNumberFromUserInput(input: string, minimumValue = 0): number | undefined { + const number = parseInt(input, 10); -export const DurationInput = React.memo(DurationInputComponent); + if (Number.isNaN(number)) { + return minimumValue; + } else { + return Math.max(minimumValue, Math.min(number, Number.MAX_SAFE_INTEGER)); + } +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/translations.ts similarity index 82% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/translations.ts index c460d2f7198b3..51d659210c52b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/duration_input/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/duration_input/translations.ts @@ -34,3 +34,10 @@ export const DAYS = i18n.translate( defaultMessage: 'Days', } ); + +export const DURATION_UNIT_SELECTOR = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRuleForm.durationUnitSelector', + { + defaultMessage: 'Duration unit selector', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx index 960df4c7de5b9..31e139a335bee 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx @@ -6,19 +6,24 @@ */ import React from 'react'; -import { - screen, - render, - act, - fireEvent, - waitFor, - waitForElementToBeRemoved, -} from '@testing-library/react'; +import { screen, render, act, fireEvent, waitFor } from '@testing-library/react'; import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; import { FIELD_TYPES, Form, useForm } from '../../../../shared_imports'; import { createReactQueryWrapper } from '../../../../common/mock'; import { fleetIntegrationsApi } from '../../../fleet_integrations/api/__mocks__'; import { RelatedIntegrations } from './related_integrations'; +import { + clearEuiComboBoxSelection, + selectEuiComboBoxOption, + selectFirstEuiComboBoxOption, + showEuiComboBoxOptions, +} from '../../../../common/test/eui/combobox'; +import { + addRelatedIntegrationRow, + removeLastRelatedIntegrationRow, + setVersion, + waitForIntegrationsToBeLoaded, +} from './test_helpers'; // must match to the import in rules/related_integrations/use_integrations.tsx jest.mock('../../../fleet_integrations/api'); @@ -41,7 +46,6 @@ const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; const COMBO_BOX_SELECTION_TEST_ID = 'euiComboBoxPill'; const COMBO_BOX_CLEAR_BUTTON_TEST_ID = 'comboBoxClearButton'; const VERSION_INPUT_TEST_ID = 'relatedIntegrationVersionDependency'; -const REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID = 'relatedIntegrationRemove'; describe('RelatedIntegrations form part', () => { beforeEach(() => { @@ -708,72 +712,6 @@ function TestForm({ initialState, onSubmit }: TestFormProps): JSX.Element { ); } -function waitForIntegrationsToBeLoaded(): Promise { - return waitForElementToBeRemoved(screen.queryAllByRole('progressbar')); -} - -function addRelatedIntegrationRow(): Promise { - return act(async () => { - fireEvent.click(screen.getByText('Add integration')); - }); -} - -function removeLastRelatedIntegrationRow(): Promise { - return act(async () => { - const lastRemoveButton = screen.getAllByTestId(REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID).at(-1); - - if (!lastRemoveButton) { - throw new Error(`There are no "${REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID}" found`); - } - - fireEvent.click(lastRemoveButton); - }); -} - -function showEuiComboBoxOptions(comboBoxToggleButton: HTMLElement): Promise { - fireEvent.click(comboBoxToggleButton); - - return waitFor(() => { - expect(screen.getByRole('listbox')).toBeInTheDocument(); - }); -} - -function selectEuiComboBoxOption({ - comboBoxToggleButton, - optionIndex, -}: { - comboBoxToggleButton: HTMLElement; - optionIndex: number; -}): Promise { - return act(async () => { - await showEuiComboBoxOptions(comboBoxToggleButton); - - fireEvent.click(screen.getAllByRole('option')[optionIndex]); - }); -} - -function clearEuiComboBoxSelection({ clearButton }: { clearButton: HTMLElement }): Promise { - return act(async () => { - fireEvent.click(clearButton); - }); -} - -function selectFirstEuiComboBoxOption({ - comboBoxToggleButton, -}: { - comboBoxToggleButton: HTMLElement; -}): Promise { - return selectEuiComboBoxOption({ comboBoxToggleButton, optionIndex: 0 }); -} - -function setVersion({ input, value }: { input: HTMLInputElement; value: string }): Promise { - return act(async () => { - fireEvent.input(input, { - target: { value }, - }); - }); -} - function submitForm(): Promise { return act(async () => { fireEvent.click(screen.getByText('Submit')); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/test_helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/test_helpers.ts new file mode 100644 index 0000000000000..b8c51fd594e13 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/test_helpers.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. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { act, fireEvent, waitForElementToBeRemoved, screen } from '@testing-library/react'; + +const REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID = 'relatedIntegrationRemove'; + +export function waitForIntegrationsToBeLoaded(): Promise { + return waitForElementToBeRemoved(screen.queryAllByRole('progressbar')); +} + +export function addRelatedIntegrationRow(): Promise { + return act(async () => { + fireEvent.click(screen.getByText('Add integration')); + }); +} + +export function removeLastRelatedIntegrationRow(): Promise { + return act(async () => { + const lastRemoveButton = screen.getAllByTestId(REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID).at(-1); + + if (!lastRemoveButton) { + throw new Error(`There are no "${REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID}" found`); + } + + fireEvent.click(lastRemoveButton); + }); +} + +export function setVersion({ + input, + value, +}: { + input: HTMLInputElement; + value: string; +}): Promise { + return act(async () => { + fireEvent.input(input, { + target: { value }, + }); + }); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/fields.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/fields.ts new file mode 100644 index 0000000000000..4956a2555bc9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/fields.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 const THRESHOLD_ALERT_SUPPRESSION_ENABLED = 'thresholdAlertSuppressionEnabled' as const; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/index.ts new file mode 100644 index 0000000000000..67848fbd5e3b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './threshold_alert_suppression_edit'; +export * from './fields'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/threshold_alert_suppression_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/threshold_alert_suppression_edit.tsx new file mode 100644 index 0000000000000..a832bff648e8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/threshold_alert_suppression_edit.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiPanel, EuiToolTip } from '@elastic/eui'; +import { CheckBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { UseField, useFormData } from '../../../../shared_imports'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from './fields'; +import { SuppressionDurationSelector } from '../alert_suppression_edit'; +import * as i18n from './translations'; + +interface ThresholdAlertSuppressionEditProps { + suppressionFieldNames: string[] | undefined; + disabled?: boolean; + disabledText?: string; +} + +export const ThresholdAlertSuppressionEdit = memo(function ThresholdAlertSuppressionEdit({ + suppressionFieldNames, + disabled, + disabledText, +}: ThresholdAlertSuppressionEditProps): JSX.Element { + const [{ [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: suppressionEnabled }] = useFormData({ + watch: THRESHOLD_ALERT_SUPPRESSION_ENABLED, + }); + const content = ( + <> + + + + + + ); + + return disabled && disabledText ? ( + + {content} + + ) : ( + content + ); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/translations.tsx new file mode 100644 index 0000000000000..25b7158610b34 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_alert_suppression_edit/translations.tsx @@ -0,0 +1,32 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const enableSuppressionForFields = (fields: string[]) => ( + {fields.join(', ')} }} + /> +); + +export const SUPPRESS_ALERTS = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.thresholdAlertSuppression.enable', + { + defaultMessage: 'Suppress alerts', + } +); + +export const THRESHOLD_SUPPRESSION_PER_RULE_EXECUTION_WARNING = i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleFields.thresholdAlertSuppression.perRuleExecutionWarning', + { + defaultMessage: 'Per rule execution option is not available for Threshold rule type', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_views.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_view_list_items.ts similarity index 81% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_views.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_view_list_items.ts index 248729f1f46e7..3d2ba5d1c3724 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_views.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/__mocks__/use_data_view_list_items.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const useDataViews = jest.fn().mockReturnValue({ +export const useDataViewListItems = jest.fn().mockReturnValue({ data: [], isFetching: false, }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx index 6cfdf060434b8..8648ade5164e6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/data_view_selector_field/data_view_selector_field.test.tsx @@ -9,14 +9,14 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import { TestProviders, useFormFieldMock } from '../../../../common/mock'; import { DataViewSelectorField } from './data_view_selector_field'; -import { useDataViews } from './use_data_views'; +import { useDataViewListItems } from './use_data_view_list_items'; jest.mock('../../../../common/lib/kibana'); -jest.mock('./use_data_views'); +jest.mock('./use_data_view_list_items'); describe('data_view_selector', () => { it('renders correctly', () => { - (useDataViews as jest.Mock).mockReturnValue({ data: [], isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: [], isFetching: false }); render( { }); it('disables the combobox while data views are fetching', () => { - (useDataViews as jest.Mock).mockReturnValue({ data: [], isFetching: true }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: [], isFetching: true }); render( { title: 'logs-*', }, ]; - (useDataViews as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); render( { title: 'logs-*', }, ]; - (useDataViews as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: dataViews, isFetching: false }); render( { }); it('displays warning on missing data view', () => { - (useDataViews as jest.Mock).mockReturnValue({ data: [], isFetching: false }); + (useDataViewListItems as jest.Mock).mockReturnValue({ data: [], isFetching: false }); render( { @@ -615,11 +615,11 @@ export const buildAlertSuppressionDescription = ( export const buildAlertSuppressionWindowDescription = ( label: string, value: Duration, - groupByRadioSelection: GroupByOptions, + alertSuppressionDuration: AlertSuppressionDurationType, ruleType: Type ): ListItems[] => { const description = - groupByRadioSelection === GroupByOptions.PerTimePeriod + alertSuppressionDuration === AlertSuppressionDurationType.PerTimePeriod ? `${value.value}${value.unit}` : i18n.ALERT_SUPPRESSION_PER_RULE_EXECUTION; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx index f5a7e39634359..de46d09065f4e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx @@ -30,6 +30,15 @@ import { schema } from '../step_about_rule/schema'; import type { ListItems } from './types'; import type { AboutStepRule } from '../../../../detections/pages/detection_engine/rules/types'; import { createLicenseServiceMock } from '../../../../../common/license/mocks'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, +} from '../../../rule_creation/components/alert_suppression_edit'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; jest.mock('../../../../common/lib/kibana'); @@ -575,25 +584,25 @@ describe('description_step', () => { describe('alert suppression', () => { const suppressionFields = { - groupByDuration: { - unit: 'm', - value: 50, + [ALERT_SUPPRESSION_DURATION_FIELD_NAME]: { + [ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME]: 50, + [ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME]: 'm', }, - groupByRadioSelection: 'per-time-period', - enableThresholdSuppression: true, - groupByFields: ['agent.name'], - suppressionMissingFields: 'suppress', + [ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME]: 'per-time-period', + [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: true, + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: ['agent.name'], + [ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME]: 'suppress', }; - describe('groupByDuration', () => { + describe(ALERT_SUPPRESSION_DURATION_FIELD_NAME, () => { ['query', 'saved_query'].forEach((ruleType) => { - test(`should be empty if groupByFields empty for ${ruleType} rule`, () => { + test(`should be empty if ${ALERT_SUPPRESSION_FIELDS_FIELD_NAME} empty for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'query', ...suppressionFields, - groupByFields: [], + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: [], }, mockFilterManager, mockLicenseService @@ -604,7 +613,7 @@ describe('description_step', () => { test(`should return item for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'query', @@ -620,7 +629,7 @@ describe('description_step', () => { test('should return item for threshold rule', () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'threshold', @@ -633,14 +642,14 @@ describe('description_step', () => { expect(result[0].description).toBe('50m'); }); - test('should return item for threshold rule if groupByFields empty', () => { + test(`should return item for threshold rule if ${ALERT_SUPPRESSION_FIELDS_FIELD_NAME} empty`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'threshold', ...suppressionFields, - groupByFields: [], + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: [], }, mockFilterManager, mockLicenseService @@ -651,12 +660,12 @@ describe('description_step', () => { test('should be empty for threshold rule if suppression not enabled', () => { const result: ListItems[] = getDescriptionItem( - 'groupByDuration', + ALERT_SUPPRESSION_DURATION_FIELD_NAME, 'label', { ruleType: 'threshold', ...suppressionFields, - enableThresholdSuppression: false, + [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: false, }, mockFilterManager, mockLicenseService @@ -666,10 +675,10 @@ describe('description_step', () => { }); }); - describe('groupByFields', () => { + describe(ALERT_SUPPRESSION_FIELDS_FIELD_NAME, () => { test(`should be empty if rule type is 'threshold'`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByFields', + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, 'label', { ruleType: 'threshold', @@ -685,7 +694,7 @@ describe('description_step', () => { ['query', 'saved_query'].forEach((ruleType) => { test(`should return item for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'groupByFields', + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, 'label', { ruleType, @@ -699,10 +708,10 @@ describe('description_step', () => { }); }); - describe('suppressionMissingFields', () => { + describe(ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, () => { test(`should be empty if rule type is 'threshold'`, () => { const result: ListItems[] = getDescriptionItem( - 'suppressionMissingFields', + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, 'label', { ruleType: 'threshold', @@ -718,7 +727,7 @@ describe('description_step', () => { ['query', 'saved_query'].forEach((ruleType) => { test(`should return item for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'suppressionMissingFields', + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, 'label', { ruleType, @@ -730,14 +739,14 @@ describe('description_step', () => { expect(result[0].description).toContain('Suppress'); }); - test(`should be empty if groupByFields empty for ${ruleType} rule`, () => { + test(`should be empty if ${ALERT_SUPPRESSION_FIELDS_FIELD_NAME} empty for ${ruleType} rule`, () => { const result: ListItems[] = getDescriptionItem( - 'suppressionMissingFields', + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, 'label', { ruleType: 'query', ...suppressionFields, - groupByFields: [], + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: [], }, mockFilterManager, mockLicenseService diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx index 4676f065f4af8..657f592fe47c4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx @@ -65,6 +65,13 @@ import { isSuppressionRuleConfiguredWithGroupBy, isSuppressionRuleConfiguredWithDuration, } from '../../../../../common/detection_engine/utils'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, +} from '../../../rule_creation/components/alert_suppression_edit'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; const DescriptionListContainer = styled(EuiDescriptionList)` max-width: 600px; @@ -217,7 +224,7 @@ export const getDescriptionItem = ( }); } else if (field === 'responseActions') { return []; - } else if (field === 'groupByFields') { + } else if (field === ALERT_SUPPRESSION_FIELDS_FIELD_NAME) { const ruleType: Type = get('ruleType', data); const ruleCanHaveGroupByFields = isSuppressionRuleConfiguredWithGroupBy(ruleType); @@ -226,9 +233,9 @@ export const getDescriptionItem = ( } const values: string[] = get(field, data); return buildAlertSuppressionDescription(label, values, ruleType); - } else if (field === 'groupByRadioSelection') { + } else if (field === ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME) { return []; - } else if (field === 'groupByDuration') { + } else if (field === ALERT_SUPPRESSION_DURATION_FIELD_NAME) { const ruleType: Type = get('ruleType', data); const ruleCanHaveDuration = isSuppressionRuleConfiguredWithDuration(ruleType); @@ -239,21 +246,21 @@ export const getDescriptionItem = ( // threshold rule has suppression duration without grouping fields, but suppression should be explicitly enabled by user // query rule have suppression duration only if group by fields selected const showDuration = isThresholdRule(ruleType) - ? get('enableThresholdSuppression', data) === true - : get('groupByFields', data).length > 0; + ? get(THRESHOLD_ALERT_SUPPRESSION_ENABLED, data) === true + : get(ALERT_SUPPRESSION_FIELDS_FIELD_NAME, data).length > 0; if (showDuration) { const value: Duration = get(field, data); return buildAlertSuppressionWindowDescription( label, value, - get('groupByRadioSelection', data), + get(ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, data), ruleType ); } else { return []; } - } else if (field === 'suppressionMissingFields') { + } else if (field === ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME) { const ruleType: Type = get('ruleType', data); const ruleCanHaveSuppressionMissingFields = isSuppressionRuleConfiguredWithMissingFields(ruleType); @@ -261,7 +268,7 @@ export const getDescriptionItem = ( if (!ruleCanHaveSuppressionMissingFields) { return []; } - if (get('groupByFields', data).length > 0) { + if (get(ALERT_SUPPRESSION_FIELDS_FIELD_NAME, data).length > 0) { const value = get(field, data); return buildAlertSuppressionMissingFieldsDescription(label, value, ruleType); } else { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts index 27dfec9818eb9..5c43b9181adcb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/translations.ts @@ -182,8 +182,8 @@ export const BUILDING_BLOCK_DESCRIPTION = i18n.translate( } ); -export const GROUP_BY_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDescription.groupByFieldsLabel', +export const ALERT_SUPPRESSION_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.alertSuppressionFieldsLabel', { defaultMessage: 'Suppress alerts by', } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx index d38af219fe858..8a27d2f668094 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/multi_select_fields/index.tsx @@ -6,11 +6,9 @@ */ import React, { useMemo } from 'react'; - -import { EuiToolTip } from '@elastic/eui'; import type { DataViewFieldBase } from '@kbn/es-query'; +import { ComboBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import type { FieldHook } from '../../../../shared_imports'; -import { Field } from '../../../../shared_imports'; import { FIELD_PLACEHOLDER } from './translations'; interface MultiSelectAutocompleteProps { @@ -18,7 +16,6 @@ interface MultiSelectAutocompleteProps { isDisabled: boolean; field: FieldHook; fullWidth?: boolean; - disabledText?: string; dataTestSubj?: string; } @@ -28,7 +25,6 @@ const fieldDescribedByIds = 'detectionEngineMultiSelectAutocompleteField'; export const MultiSelectAutocompleteComponent: React.FC = ({ browserFields, - disabledText, isDisabled, field, fullWidth = false, @@ -46,21 +42,15 @@ export const MultiSelectAutocompleteComponent: React.FC ); - return isDisabled ? ( - - {fieldComponent} - - ) : ( - fieldComponent - ); }; export const MultiSelectFieldsAutocomplete = React.memo(MultiSelectAutocompleteComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx index bdbc01ada58ff..cc303731b26e3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.test.tsx @@ -23,7 +23,7 @@ import type { } from '../../../../detections/pages/detection_engine/rules/types'; import { DataSourceType, - GroupByOptions, + AlertSuppressionDurationType, } from '../../../../detections/pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../../detections/pages/detection_engine/rules/helpers'; import { TestProviders } from '../../../../common/mock'; @@ -36,6 +36,16 @@ import { import type { FormHook } from '../../../../shared_imports'; import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; import { useKibana } from '../../../../common/lib/kibana'; +import { + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME, + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, +} from '../../../rule_creation/components/alert_suppression_edit'; +import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; +import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/containers/source'); @@ -69,16 +79,17 @@ export const stepDefineStepMLRule: DefineStepRule = { timeline: { id: null, title: null }, eqlOptions: {}, dataSourceType: DataSourceType.IndexPatterns, - groupByFields: ['host.name'], - groupByRadioSelection: GroupByOptions.PerRuleExecution, - groupByDuration: { - unit: 'm', - value: 5, + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: ['host.name'], + [ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME]: AlertSuppressionDurationType.PerRuleExecution, + [ALERT_SUPPRESSION_DURATION_FIELD_NAME]: { + [ALERT_SUPPRESSION_DURATION_VALUE_FIELD_NAME]: 5, + [ALERT_SUPPRESSION_DURATION_UNIT_FIELD_NAME]: 'm', }, + [ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME]: AlertSuppressionMissingFieldsStrategyEnum.suppress, + [THRESHOLD_ALERT_SUPPRESSION_ENABLED]: false, newTermsFields: ['host.ip'], historyWindowSize: '7d', shouldLoadQueryDynamically: false, - enableThresholdSuppression: false, }; describe('StepAboutRuleComponent', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index f1dcfc74e7923..50264fffabfb8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -9,7 +9,8 @@ import React, { useEffect, useState } from 'react'; import { screen, fireEvent, render, within, act, waitFor } from '@testing-library/react'; import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-types'; import type { DataViewBase } from '@kbn/es-query'; -import { StepDefineRule, aggregatableFields } from '.'; +import type { FieldSpec } from '@kbn/data-plugin/common'; +import { StepDefineRule } from '.'; import type { StepDefineRuleProps } from '.'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { useRuleFromTimeline } from '../../../../detections/containers/detection_engine/rules/use_rule_from_timeline'; @@ -25,6 +26,22 @@ import { createIndexPatternField, getSelectToggleButtonForName, } from '../../../rule_creation/components/required_fields/required_fields.test'; +import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit'; +import { + expectDuration, + expectSuppressionFields, + setDuration, + setDurationType, + setSuppressionFields, +} from '../../../rule_creation/components/alert_suppression_edit/test_helpers'; +import { + selectEuiComboBoxOption, + selectFirstEuiComboBoxOption, +} from '../../../../common/test/eui/combobox'; +import { + addRelatedIntegrationRow, + setVersion, +} from '../../../rule_creation/components/related_integrations/test_helpers'; // Mocks integrations jest.mock('../../../fleet_integrations/api'); @@ -48,7 +65,13 @@ jest.mock('../ai_assistant', () => { }; }); -jest.mock('../data_view_selector_field/use_data_views'); +jest.mock('../data_view_selector_field/use_data_view_list_items'); + +jest.mock('../../../../common/hooks/use_license', () => ({ + useLicense: jest.fn().mockReturnValue({ + isAtLeast: jest.fn().mockReturnValue(true), + }), +})); const mockRedirectLegacyUrl = jest.fn(); const mockGetLegacyUrlConflict = jest.fn(); @@ -149,53 +172,6 @@ jest.mock('react-redux', () => { jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_from_timeline'); -test('aggregatableFields', function () { - expect( - aggregatableFields([ - { - name: 'error.message', - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - readFromDocValues: false, - }, - ]) - ).toEqual([]); -}); - -test('aggregatableFields with aggregatable: true', function () { - expect( - aggregatableFields([ - { - name: 'error.message', - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - readFromDocValues: false, - }, - { - name: 'file.path', - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - readFromDocValues: false, - }, - ]) - ).toEqual([ - { - name: 'file.path', - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - readFromDocValues: false, - }, - ]); -}); - const mockUseRuleFromTimeline = useRuleFromTimeline as jest.Mock; const onOpenTimeline = jest.fn(); @@ -218,6 +194,62 @@ describe.skip('StepDefineRule', () => { expect(screen.getByTestId('stepDefineRule')).toBeDefined(); }); + describe('alert suppression', () => { + it('persists state when switching between custom query and threshold rule types', async () => { + const mockFields: FieldSpec[] = [ + { + name: 'test-field', + type: 'string', + searchable: false, + aggregatable: true, + }, + ]; + + const { rerender } = render( + , + { + wrapper: TestProviders, + } + ); + + await setSuppressionFields(['test-field']); + setDurationType('Per time period'); + setDuration(10, 'h'); + + // switch to threshold rule type + rerender( + + ); + + expectDuration(10, 'h'); + + // switch back to custom query rule type + rerender( + + ); + + expectSuppressionFields(['test-field']); + expectDuration(10, 'h'); + }); + }); + describe('related integrations', () => { beforeEach(() => { fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ @@ -631,13 +663,12 @@ function TestForm({ ruleType={ruleType} index={stepDefineDefaultValue.index} threatIndex={stepDefineDefaultValue.threatIndex} - groupByFields={stepDefineDefaultValue.groupByFields} + alertSuppressionFields={stepDefineDefaultValue[ALERT_SUPPRESSION_FIELDS_FIELD_NAME]} dataSourceType={stepDefineDefaultValue.dataSourceType} shouldLoadQueryDynamically={stepDefineDefaultValue.shouldLoadQueryDynamically} queryBarTitle="" queryBarSavedId="" thresholdFields={[]} - enableThresholdSuppression={false} {...formProps} />