diff --git a/.buildkite/pipeline-resource-definitions/kibana-vm-images.yml b/.buildkite/pipeline-resource-definitions/kibana-vm-images.yml new file mode 100644 index 0000000000000..dd8a6c945c455 --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-vm-images.yml @@ -0,0 +1,48 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-vm-images + description: Build CI agent VM images for Kibana + links: + - url: 'https://buildkite.com/elastic/kibana-vm-images' + title: Pipeline link +spec: + type: buildkite-pipeline + owner: group:kibana-operations + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / vm images + description: Build CI agent VM images for Kibana + spec: + env: + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-operations-alerts' + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + default_branch: main + repository: elastic/ci-agent-images + pipeline_file: vm-images/.buildkite/pipeline.yml + skip_intermediate_builds: false + provider_settings: + trigger_mode: none + schedules: + daily kibana image build: + branch: main + cronline: '0 0 * * *' + env: + IMAGES_CONFIG: kibana/images.yml + message: Builds Kibana VM images daily + daily kibana fips image build: + branch: main + cronline: '0 4 * * *' # make sure this runs after the daily kibana image build + env: + BASE_IMAGES_CONFIG: 'core/images.yml,kibana/images.yml' + IMAGES_CONFIG: kibana/fips.yml + message: Builds Kibana FIPS VM image daily + teams: + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + everyone: + access_level: BUILD_AND_READ diff --git a/.buildkite/pipeline-resource-definitions/locations.yml b/.buildkite/pipeline-resource-definitions/locations.yml index ce0ab7750d489..7f96bff2b51b4 100644 --- a/.buildkite/pipeline-resource-definitions/locations.yml +++ b/.buildkite/pipeline-resource-definitions/locations.yml @@ -37,6 +37,7 @@ spec: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-vm-images.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-ess/security-solution-ess.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-defend-workflows.yml diff --git a/.eslintrc.js b/.eslintrc.js index e46dde5a3c56f..006f39ce1026c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1014,6 +1014,7 @@ module.exports = { 'error', { patterns: ['**/legacy_uptime/*'], + paths: RESTRICTED_IMPORTS, }, ], }, @@ -1055,6 +1056,7 @@ module.exports = { { // prevents UI code from importing server side code and then webpack including it when doing builds patterns: ['**/server/*'], + paths: RESTRICTED_IMPORTS, }, ], }, @@ -1113,6 +1115,7 @@ module.exports = { { // prevents UI code from importing server side code and then webpack including it when doing builds patterns: ['**/server/*'], + paths: RESTRICTED_IMPORTS, }, ], }, @@ -1184,13 +1187,7 @@ module.exports = { // to help deprecation and prevent accidental re-use/continued use of code we plan on removing. If you are // finding yourself turning this off a lot for "new code" consider renaming the file and functions if it is has valid uses. patterns: ['*legacy*'], - paths: [ - { - name: 'react-router-dom', - importNames: ['Route'], - message: "import { Route } from '@kbn/kibana-react-plugin/public'", - }, - ], + paths: RESTRICTED_IMPORTS, }, ], }, @@ -1348,6 +1345,7 @@ module.exports = { { // prevents UI code from importing server side code and then webpack including it when doing builds patterns: ['**/server/*'], + paths: RESTRICTED_IMPORTS, }, ], }, @@ -1525,6 +1523,7 @@ module.exports = { // to help deprecation and prevent accidental re-use/continued use of code we plan on removing. If you are // finding yourself turning this off a lot for "new code" consider renaming the file and functions if it has valid uses. patterns: ['*legacy*'], + paths: RESTRICTED_IMPORTS, }, ], }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 241593811f941..f126ad0cad658 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1327,6 +1327,8 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/dev-tools @elastic/kibana-operations /catalog-info.yaml @elastic/kibana-operations @elastic/kibana-tech-leads /.devcontainer/ @elastic/kibana-operations +/.eslintrc.js @elastic/kibana-operations +/.eslintignore @elastic/kibana-operations # Appex QA /x-pack/test_serverless/tsconfig.json @elastic/appex-qa @@ -1394,9 +1396,9 @@ x-pack/test_serverless/api_integration/test_suites/common/security_response_head # Kibana Telemetry /.telemetryrc.json @elastic/kibana-core /x-pack/.telemetryrc.json @elastic/kibana-core -/src/plugins/telemetry/schema/ @elastic/kibana-core @elastic/kibana-telemetry -/x-pack/plugins/telemetry_collection_xpack/schema/ @elastic/kibana-core @elastic/kibana-telemetry -x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kibana-core @elastic/kibana-telemetry @shahinakmal +/src/plugins/telemetry/schema/ @elastic/kibana-core +/x-pack/plugins/telemetry_collection_xpack/schema/ @elastic/kibana-core +x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kibana-core @shahinakmal # Kibana Localization /src/dev/i18n_tools/ @elastic/kibana-localization @elastic/kibana-core @@ -1750,6 +1752,8 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/plugins/security_solution/common/api/detection_engine/signals_migration @elastic/security-detection-engine /x-pack/plugins/security_solution/common/cti @elastic/security-detection-engine /x-pack/plugins/security_solution/common/field_maps @elastic/security-detection-engine +/x-pack/test/functional/es_archives/entity/risks @elastic/security-detection-engine +/x-pack/test/functional/es_archives/entity/host_risk @elastic/security-detection-engine /x-pack/plugins/security_solution/public/sourcerer @elastic/security-threat-hunting-investigations /x-pack/plugins/security_solution/public/detection_engine/rule_creation @elastic/security-detection-engine diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 34a5103cba9fb..744763f3da424 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -5077,6 +5077,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Mute all alerts", @@ -5124,6 +5133,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Unmute all alerts", diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 4719fcb479bb5..b73fa1fc22841 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -5077,6 +5077,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Mute all alerts", @@ -5124,6 +5133,15 @@ "responses": { "204": { "description": "Indicates a successful call." + }, + "400": { + "description": "Indicates an invalid schema or parameters." + }, + "403": { + "description": "Indicates that this call is forbidden." + }, + "404": { + "description": "Indicates a rule with the given ID does not exist." } }, "summary": "Unmute all alerts", diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 9e63182949f25..a4362db15cc7d 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -4217,6 +4217,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Mute all alerts tags: - alerting @@ -4248,6 +4254,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Unmute all alerts tags: - alerting diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 9e63182949f25..a4362db15cc7d 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -4217,6 +4217,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Mute all alerts tags: - alerting @@ -4248,6 +4254,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Unmute all alerts tags: - alerting diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index f32de75a62b26..16a6a94d34d81 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -4598,6 +4598,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Mute all alerts tags: - alerting @@ -4629,6 +4635,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Unmute all alerts tags: - alerting diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index f32de75a62b26..16a6a94d34d81 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -4598,6 +4598,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Mute all alerts tags: - alerting @@ -4629,6 +4635,12 @@ paths: responses: '204': description: Indicates a successful call. + '400': + description: Indicates an invalid schema or parameters. + '403': + description: Indicates that this call is forbidden. + '404': + description: Indicates a rule with the given ID does not exist. summary: Unmute all alerts tags: - alerting diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts index 8f4e59d80458b..9a719c24076f7 100644 --- a/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts +++ b/packages/kbn-alerts-ui-shared/src/common/apis/update_rule/transform_update_rule_body.ts @@ -54,6 +54,6 @@ export const transformUpdateRuleBody: RewriteResponseCase = ({ ...(uuid && { uuid }), }; }), - ...(alertDelay ? { alert_delay: alertDelay } : {}), + ...(alertDelay !== undefined ? { alert_delay: alertDelay } : {}), ...(flapping !== undefined ? { flapping: transformUpdateRuleFlapping(flapping) } : {}), }); diff --git a/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts index 49ea5a63b3fca..542bb055fd431 100644 --- a/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts +++ b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts @@ -8,4 +8,4 @@ */ // Feature flag for frontend rule specific flapping in rule flyout -export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false; +export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = true; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts index 4ee00a94b90ed..ebdfeeafbe2fd 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_create_rule.ts @@ -10,10 +10,11 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpStart, IHttpFetchError } from '@kbn/core-http-browser'; import { createRule, CreateRuleBody } from '../apis/create_rule'; +import { Rule } from '../types'; export interface UseCreateRuleProps { http: HttpStart; - onSuccess?: (formData: CreateRuleBody) => void; + onSuccess?: (rule: Rule) => void; onError?: (error: IHttpFetchError<{ message: string }>) => void; } diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts index 9ae876d06278b..8c93881762b1a 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_connectors.ts @@ -15,10 +15,11 @@ export interface UseLoadConnectorsProps { http: HttpStart; includeSystemActions?: boolean; enabled?: boolean; + cacheTime?: number; } export const useLoadConnectors = (props: UseLoadConnectorsProps) => { - const { http, includeSystemActions = false, enabled = true } = props; + const { http, includeSystemActions = false, enabled = true, cacheTime } = props; const queryFn = () => { return fetchConnectors({ http, includeSystemActions }); @@ -27,6 +28,7 @@ export const useLoadConnectors = (props: UseLoadConnectorsProps) => { const { data, isLoading, isFetching, isInitialLoading } = useQuery({ queryKey: ['useLoadConnectors', includeSystemActions], queryFn, + cacheTime, refetchOnWindowFocus: false, enabled, }); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts index fab6fd3336f2e..c9dbc6c75ff35 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_type_aad_template_fields.ts @@ -17,10 +17,11 @@ export interface UseLoadRuleTypeAadTemplateFieldProps { http: HttpStart; ruleTypeId?: string; enabled: boolean; + cacheTime?: number; } export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplateFieldProps) => { - const { http, ruleTypeId, enabled } = props; + const { http, ruleTypeId, enabled, cacheTime } = props; const queryFn = () => { if (!ruleTypeId) { @@ -43,6 +44,7 @@ export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplat description: getDescription(d.name, EcsFlat), })); }, + cacheTime, refetchOnWindowFocus: false, enabled, }); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts index fafd372dc3640..95c3ca6baad02 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_resolve_rule.ts @@ -15,10 +15,11 @@ import { RuleFormData } from '../../rule_form'; export interface UseResolveProps { http: HttpStart; id?: string; + cacheTime?: number; } export const useResolveRule = (props: UseResolveProps) => { - const { id, http } = props; + const { id, http, cacheTime } = props; const queryFn = () => { if (id) { @@ -30,6 +31,7 @@ export const useResolveRule = (props: UseResolveProps) => { queryKey: ['useResolveRule', id], queryFn, enabled: !!id, + cacheTime, select: (rule): RuleFormData | null => { if (!rule) { return null; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts index 0e8199fc1cca2..5764b8128ef42 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_update_rule.ts @@ -10,10 +10,11 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpStart, IHttpFetchError } from '@kbn/core-http-browser'; import { updateRule, UpdateRuleBody } from '../apis/update_rule'; +import { Rule } from '../types'; export interface UseUpdateRuleProps { http: HttpStart; - onSuccess?: (formData: UpdateRuleBody) => void; + onSuccess?: (rule: Rule) => void; onError?: (error: IHttpFetchError<{ message: string }>) => void; } diff --git a/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts b/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts index 40498f1a27886..29eaf17552a2b 100644 --- a/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts +++ b/packages/kbn-alerts-ui-shared/src/common/types/rule_types.ts @@ -27,8 +27,6 @@ import { TypeRegistry } from '../type_registry'; export type { SanitizedRuleAction as RuleAction } from '@kbn/alerting-types'; -export type { Flapping } from '@kbn/alerting-types'; - export type RuleTypeWithDescription = RuleType & { description?: string }; export type RuleTypeIndexWithDescriptions = Map; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts b/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts index f557dc5ebdb42..a3748eeabe697 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts @@ -27,7 +27,7 @@ export const DEFAULT_FREQUENCY = { summary: false, }; -export const GET_DEFAULT_FORM_DATA = ({ +export const getDefaultFormData = ({ ruleTypeId, name, consumer, @@ -50,6 +50,7 @@ export const GET_DEFAULT_FORM_DATA = ({ ruleTypeId, name, actions, + alertDelay: { active: 1 }, }; }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx index fc96ae214a7a8..4399dc5239ec7 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx @@ -12,7 +12,7 @@ import { EuiLoadingElastic } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import type { RuleFormData, RuleFormPlugins } from './types'; -import { DEFAULT_VALID_CONSUMERS, GET_DEFAULT_FORM_DATA } from './constants'; +import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; import { RuleFormStateProvider } from './rule_form_state'; import { useCreateRule } from '../common/hooks'; import { RulePage } from './rule_page'; @@ -24,6 +24,7 @@ import { } from './rule_form_errors'; import { useLoadDependencies } from './hooks/use_load_dependencies'; import { + getAvailableRuleTypes, getInitialConsumer, getInitialMultiConsumer, getInitialSchedule, @@ -42,7 +43,8 @@ export interface CreateRuleFormProps { shouldUseRuleProducer?: boolean; canShowConsumerSelection?: boolean; showMustacheAutocompleteSwitch?: boolean; - returnUrl: string; + onCancel?: () => void; + onSubmit?: (ruleId: string) => void; } export const CreateRuleForm = (props: CreateRuleFormProps) => { @@ -56,7 +58,8 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { shouldUseRuleProducer = false, canShowConsumerSelection = true, showMustacheAutocompleteSwitch = false, - returnUrl, + onCancel, + onSubmit, } = props; const { http, docLinks, notifications, ruleTypeRegistry, i18n, theme } = plugins; @@ -64,8 +67,9 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { const { mutate, isLoading: isSaving } = useCreateRule({ http, - onSuccess: ({ name }) => { + onSuccess: ({ name, id }) => { toasts.addSuccess(RULE_CREATE_SUCCESS_TEXT(name)); + onSubmit?.(id); }, onError: (error) => { const message = parseRuleCircuitBreakerErrorMessage( @@ -86,6 +90,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { const { isInitialLoading, ruleType, + ruleTypes, ruleTypeModel, uiConfig, healthCheckError, @@ -153,7 +158,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
{ minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleTypeModel: ruleTypeModel, selectedRuleType: ruleType, + availableRuleTypes: getAvailableRuleTypes({ + consumer, + ruleTypes, + ruleTypeRegistry, + }).map(({ ruleType: rt }) => rt), validConsumers, flappingSettings, canShowConsumerSelection, @@ -185,7 +195,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), }} > - +
); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx index 6e92b94cc2e0d..917fc87420f9a 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx @@ -24,17 +24,19 @@ import { RuleFormRuleTypeError, } from './rule_form_errors'; import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations'; -import { parseRuleCircuitBreakerErrorMessage } from './utils'; +import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils'; +import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; export interface EditRuleFormProps { id: string; plugins: RuleFormPlugins; showMustacheAutocompleteSwitch?: boolean; - returnUrl: string; + onCancel?: () => void; + onSubmit?: (ruleId: string) => void; } export const EditRuleForm = (props: EditRuleFormProps) => { - const { id, plugins, returnUrl, showMustacheAutocompleteSwitch = false } = props; + const { id, plugins, showMustacheAutocompleteSwitch = false, onCancel, onSubmit } = props; const { http, notifications, docLinks, ruleTypeRegistry, i18n, theme, application } = plugins; const { toasts } = notifications; @@ -42,6 +44,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { http, onSuccess: ({ name }) => { toasts.addSuccess(RULE_EDIT_SUCCESS_TEXT(name)); + onSubmit?.(id); }, onError: (error) => { const message = parseRuleCircuitBreakerErrorMessage( @@ -62,6 +65,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { const { isInitialLoading, ruleType, + ruleTypes, ruleTypeModel, uiConfig, healthCheckError, @@ -156,17 +160,31 @@ export const EditRuleForm = (props: EditRuleFormProps) => { connectors, connectorTypes, aadTemplateFields, - formData: fetchedFormData, + formData: { + ...getDefaultFormData({ + ruleTypeId: fetchedFormData.ruleTypeId, + name: fetchedFormData.name, + consumer: fetchedFormData.consumer, + actions: fetchedFormData.actions, + }), + ...fetchedFormData, + }, id, plugins, minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleType: ruleType, selectedRuleTypeModel: ruleTypeModel, + availableRuleTypes: getAvailableRuleTypes({ + consumer: fetchedFormData.consumer, + ruleTypes, + ruleTypeRegistry, + }).map(({ ruleType: rt }) => rt), flappingSettings, + validConsumers: DEFAULT_VALID_CONSUMERS, showMustacheAutocompleteSwitch, }} > - + ); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx index 9d2ce3b6f1211..f0a14ac82e4a6 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx @@ -46,10 +46,6 @@ jest.mock('../../common/hooks/use_load_rule_type_aad_template_fields', () => ({ useLoadRuleTypeAadTemplateField: jest.fn(), })); -jest.mock('../utils/get_authorized_rule_types', () => ({ - getAvailableRuleTypes: jest.fn(), -})); - jest.mock('../../common/hooks/use_fetch_flapping_settings', () => ({ useFetchFlappingSettings: jest.fn(), })); @@ -63,7 +59,6 @@ const { useLoadRuleTypeAadTemplateField } = jest.requireMock( '../../common/hooks/use_load_rule_type_aad_template_fields' ); const { useLoadRuleTypesQuery } = jest.requireMock('../../common/hooks/use_load_rule_types_query'); -const { getAvailableRuleTypes } = jest.requireMock('../utils/get_authorized_rule_types'); const { useFetchFlappingSettings } = jest.requireMock( '../../common/hooks/use_fetch_flapping_settings' ); @@ -168,13 +163,6 @@ useLoadRuleTypesQuery.mockReturnValue({ }, }); -getAvailableRuleTypes.mockReturnValue([ - { - ruleType: indexThresholdRuleType, - ruleTypeModel: indexThresholdRuleTypeModel, - }, -]); - const mockConnector = { id: 'test-connector', name: 'Test', @@ -236,7 +224,7 @@ const toastsMock = jest.fn(); const ruleTypeRegistryMock: RuleTypeRegistryContract = { has: jest.fn(), register: jest.fn(), - get: jest.fn(), + get: jest.fn().mockReturnValue(indexThresholdRuleTypeModel), list: jest.fn(), }; @@ -272,6 +260,7 @@ describe('useLoadDependencies', () => { isLoading: false, isInitialLoading: false, ruleType: indexThresholdRuleType, + ruleTypes: [...ruleTypeIndex.values()], ruleTypeModel: indexThresholdRuleTypeModel, uiConfig: uiConfigMock, healthCheckError: null, @@ -317,39 +306,6 @@ describe('useLoadDependencies', () => { }); }); - test('should call getAvailableRuleTypes with the correct params', async () => { - const { result } = renderHook( - () => { - return useLoadDependencies({ - http: httpMock as unknown as HttpStart, - toasts: toastsMock as unknown as ToastsStart, - ruleTypeRegistry: ruleTypeRegistryMock, - validConsumers: ['stackAlerts', 'logs'], - consumer: 'logs', - capabilities: { - actions: { - show: true, - save: true, - execute: true, - }, - } as unknown as ApplicationStart['capabilities'], - }); - }, - { wrapper } - ); - - await waitFor(() => { - return expect(result.current.isInitialLoading).toEqual(false); - }); - - expect(getAvailableRuleTypes).toBeCalledWith({ - consumer: 'logs', - ruleTypeRegistry: ruleTypeRegistryMock, - ruleTypes: [indexThresholdRuleType], - validConsumers: ['stackAlerts', 'logs'], - }); - }); - test('should call resolve rule with the correct params', async () => { const { result } = renderHook( () => { @@ -377,6 +333,7 @@ describe('useLoadDependencies', () => { expect(useResolveRule).toBeCalledWith({ http: httpMock, id: 'test-rule-id', + cacheTime: 0, }); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts index 5e0c52b1089ba..9fb0f173b9d21 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts @@ -20,7 +20,6 @@ import { useLoadUiConfig, useResolveRule, } from '../../common/hooks'; -import { getAvailableRuleTypes } from '../utils'; import { RuleTypeRegistryContract } from '../../common'; import { useFetchFlappingSettings } from '../../common/hooks/use_fetch_flapping_settings'; import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping'; @@ -43,8 +42,6 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { http, toasts, ruleTypeRegistry, - consumer, - validConsumers, id, ruleTypeId, capabilities, @@ -69,7 +66,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { data: fetchedFormData, isLoading: isLoadingRule, isInitialLoading: isInitialLoadingRule, - } = useResolveRule({ http, id }); + } = useResolveRule({ http, id, cacheTime: 0 }); const { ruleTypesState: { @@ -100,6 +97,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { http, includeSystemActions: true, enabled: canReadConnectors, + cacheTime: 0, }); const computedRuleTypeId = useMemo(() => { @@ -125,28 +123,22 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { http, ruleTypeId: computedRuleTypeId, enabled: !!computedRuleTypeId && canReadConnectors, + cacheTime: 0, }); - const authorizedRuleTypeItems = useMemo(() => { - const computedConsumer = consumer || fetchedFormData?.consumer; - if (!computedConsumer) { - return []; + const ruleType = useMemo(() => { + if (!computedRuleTypeId || !ruleTypeIndex) { + return null; } - return getAvailableRuleTypes({ - consumer: computedConsumer, - ruleTypes: [...ruleTypeIndex.values()], - ruleTypeRegistry, - validConsumers, - }); - }, [consumer, ruleTypeIndex, ruleTypeRegistry, validConsumers, fetchedFormData]); - - const [ruleType, ruleTypeModel] = useMemo(() => { - const item = authorizedRuleTypeItems.find(({ ruleType: rt }) => { - return rt.id === computedRuleTypeId; - }); - - return [item?.ruleType, item?.ruleTypeModel]; - }, [authorizedRuleTypeItems, computedRuleTypeId]); + return ruleTypeIndex.get(computedRuleTypeId); + }, [computedRuleTypeId, ruleTypeIndex]); + + const ruleTypeModel = useMemo(() => { + if (!computedRuleTypeId) { + return null; + } + return ruleTypeRegistry.get(computedRuleTypeId); + }, [computedRuleTypeId, ruleTypeRegistry]); const isLoading = useMemo(() => { // Create Mode @@ -227,6 +219,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoading: !!isInitialLoading, ruleType, ruleTypeModel, + ruleTypes: [...ruleTypeIndex.values()], uiConfig, healthCheckError, fetchedFormData, diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx index 63846fb3628ce..9560d933060f6 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx @@ -117,12 +117,18 @@ describe('ruleActions', () => { getActionTypeModel('1', { id: 'actionType-1', validateParams: mockValidate, + defaultActionParams: { + key: 'value', + }, }) ); actionTypeRegistry.register( getActionTypeModel('2', { id: 'actionType-2', validateParams: mockValidate, + defaultActionParams: { + key: 'value', + }, }) ); @@ -150,6 +156,10 @@ describe('ruleActions', () => { selectedRuleType: { id: 'selectedRuleTypeId', defaultActionGroupId: 'test', + recoveryActionGroup: { + id: 'test-recovery-group-id', + name: 'test-recovery-group', + }, producer: 'stackAlerts', }, connectors: mockConnectors, @@ -222,7 +232,7 @@ describe('ruleActions', () => { frequency: { notifyWhen: 'onActionGroupChange', summary: false, throttle: null }, group: 'test', id: 'connector-1', - params: {}, + params: { key: 'value' }, uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', }, type: 'addAction', diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx index b9eb28025205c..47588b487be6d 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx @@ -18,6 +18,7 @@ import { ActionConnector, RuleAction, RuleFormParamsErrors } from '../../common/ import { DEFAULT_FREQUENCY, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; import { RuleActionsItem } from './rule_actions_item'; import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item'; +import { getDefaultParams } from '../utils'; export const RuleActions = () => { const [isConnectorModalOpen, setIsConnectorModalOpen] = useState(false); @@ -44,7 +45,15 @@ export const RuleActions = () => { async (connector: ActionConnector) => { const { id, actionTypeId } = connector; const uuid = uuidv4(); - const params = {}; + const group = selectedRuleType.defaultActionGroupId; + const actionTypeModel = actionTypeRegistry.get(actionTypeId); + + const params = + getDefaultParams({ + group, + ruleType: selectedRuleType, + actionTypeModel, + }) || {}; dispatch({ type: 'addAction', @@ -53,7 +62,7 @@ export const RuleActions = () => { actionTypeId, uuid, params, - group: selectedRuleType.defaultActionGroupId, + group, frequency: DEFAULT_FREQUENCY, }, }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx index 791c1ce0491f2..a5bbacc74d7a5 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_alerts_filter.tsx @@ -68,6 +68,7 @@ export const RuleActionsAlertsFilter = ({ () => onChange(state ? undefined : query), [state, query, onChange] ); + const updateQuery = useCallback( (update: Partial) => { setQuery({ diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx index 9c3dbcf15e364..82496d9578ff0 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_connectors_modal.tsx @@ -163,7 +163,10 @@ export const RuleActionsConnectorsModal = (props: RuleActionsConnectorsModalProp const connectorFacetButtons = useMemo(() => { return ( - + { await userEvent.click(screen.getByText('onTimeframeChange')); - expect(mockOnChange).toHaveBeenCalledTimes(1); + expect(mockOnChange).toHaveBeenCalledTimes(2); expect(mockOnChange).toHaveBeenCalledWith({ payload: { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx index b80a79a69cfcf..9bf6cac970b19 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions_item.tsx @@ -40,17 +40,12 @@ import { isEmpty, some } from 'lodash'; import { css } from '@emotion/react'; import { SavedObjectAttribute } from '@kbn/core/types'; import { useRuleFormDispatch, useRuleFormState } from '../hooks'; -import { - ActionConnector, - ActionTypeModel, - RuleFormParamsErrors, - RuleTypeWithDescription, -} from '../../common/types'; +import { ActionConnector, RuleFormParamsErrors } from '../../common/types'; import { getAvailableActionVariables } from '../../action_variables'; import { validateAction, validateParamsForWarnings } from '../validation'; import { RuleActionsSettings } from './rule_actions_settings'; -import { getSelectedActionGroup } from '../utils'; +import { getDefaultParams, getSelectedActionGroup } from '../utils'; import { RuleActionsMessage } from './rule_actions_message'; import { ACTION_ERROR_TOOLTIP, @@ -60,6 +55,7 @@ import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL, } from '../translations'; +import { checkActionFormActionTypeEnabled } from '../utils/check_action_type_enabled'; const SUMMARY_GROUP_TITLE = i18n.translate('alertsUIShared.ruleActionsItem.summaryGroupTitle', { defaultMessage: 'Summary of alerts', @@ -83,22 +79,6 @@ const ACTION_TITLE = (connector: ActionConnector) => }, }); -const getDefaultParams = ({ - group, - ruleType, - actionTypeModel, -}: { - group: string; - actionTypeModel: ActionTypeModel; - ruleType: RuleTypeWithDescription; -}) => { - if (group === ruleType.recoveryActionGroup.id) { - return actionTypeModel.defaultRecoveredActionParams; - } else { - return actionTypeModel.defaultActionParams; - } -}; - export interface RuleActionsItemProps { action: RuleAction; index: number; @@ -178,6 +158,16 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { ? aadTemplateFields : availableActionVariables; + const checkEnabledResult = useMemo(() => { + if (!actionType) { + return null; + } + return checkActionFormActionTypeEnabled( + actionType, + connectors.filter((c) => c.isPreconfigured) + ); + }, [actionType, connectors]); + const onDelete = (id: string) => { dispatch({ type: 'removeAction', payload: { uuid: id } }); }; @@ -381,16 +371,24 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { ...action.alertsFilter, query, }; + + if (!newAlertsFilter.query) { + delete newAlertsFilter.query; + } + + const alertsFilter = isEmpty(newAlertsFilter) ? undefined : newAlertsFilter; + const newAction = { ...action, - alertsFilter: newAlertsFilter, + alertsFilter, }; + dispatch({ type: 'setActionProperty', payload: { uuid: action.uuid!, key: 'alertsFilter', - value: newAlertsFilter, + value: alertsFilter, }, }); validateActionBase(newAction); @@ -400,19 +398,33 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { const onTimeframeChange = useCallback( (timeframe?: AlertsFilterTimeframe) => { + const newAlertsFilter = { + ...action.alertsFilter, + timeframe, + }; + + if (!newAlertsFilter.timeframe) { + delete newAlertsFilter.timeframe; + } + + const alertsFilter = isEmpty(newAlertsFilter) ? undefined : newAlertsFilter; + + const newAction = { + ...action, + alertsFilter, + }; + dispatch({ type: 'setActionProperty', payload: { uuid: action.uuid!, key: 'alertsFilter', - value: { - ...action.alertsFilter, - timeframe, - }, + value: alertsFilter, }, }); + validateActionBase(newAction); }, - [action, dispatch] + [action, dispatch, validateActionBase] ); const onUseAadTemplateFieldsChange = useCallback(() => { @@ -443,9 +455,25 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { }, [action, storedActionParamsForAadToggle, dispatch]); const accordionContent = useMemo(() => { - if (!connector) { + if (!connector || !checkEnabledResult) { return null; } + + if (!checkEnabledResult.isEnabled) { + return ( + + {checkEnabledResult.messageCard} + + ); + } + return ( { templateFields, useDefaultMessage, warning, + checkEnabledResult, onNotifyWhenChange, onActionGroupChange, onAlertsFilterChange, diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx index 7b12160c1dadd..327a0ba12634c 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx @@ -74,17 +74,14 @@ describe('RuleAlertDelay', () => { expect(mockOnChange).not.toHaveBeenCalled(); }); - test('Should call onChange with null if empty string is typed', () => { + test('Should not call onChange if empty string is typed', () => { render(); fireEvent.change(screen.getByTestId('alertDelayInput'), { target: { value: '', }, }); - expect(mockOnChange).toHaveBeenCalledWith({ - type: 'setAlertDelay', - payload: null, - }); + expect(mockOnChange).not.toHaveBeenCalled(); }); test('Should display error when input is invalid', () => { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx index 5b26c38232ab4..a79f1f5efe447 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx @@ -28,16 +28,8 @@ export const RuleAlertDelay = () => { const onAlertDelayChange = useCallback( (e: React.ChangeEvent) => { - if (!e.target.validity.valid) { - return; - } - const value = e.target.value; - if (value === '') { - dispatch({ - type: 'setAlertDelay', - payload: null, - }); - } else if (INTEGER_REGEX.test(value)) { + const value = e.target.value.trim(); + if (INTEGER_REGEX.test(value)) { const parsedValue = parseInt(value, 10); dispatch({ type: 'setAlertDelay', @@ -66,7 +58,7 @@ export const RuleAlertDelay = () => { { active: 5, }, notifyWhen: null, - consumer: 'stackAlerts', + consumer: 'alerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], + availableRuleTypes: [ruleType], }); render(); @@ -164,13 +167,16 @@ describe('Rule Definition', () => { active: 5, }, notifyWhen: null, - consumer: 'stackAlerts', + consumer: 'alerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: { ...ruleModel, documentationUrl: null, }, + availableRuleTypes: [ruleType], + validConsumers: ['logs', 'stackAlerts'], }); render(); @@ -191,6 +197,7 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, @@ -215,9 +222,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelect: true, validConsumers: ['logs'], }); @@ -241,9 +250,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelect: true, validConsumers: ['logs', 'observability'], }); @@ -267,9 +278,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], }); render(); @@ -292,9 +305,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], }); render(); @@ -326,9 +341,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], }); @@ -339,6 +356,48 @@ describe('Rule Definition', () => { expect(screen.getByTestId('ruleSettingsFlappingForm')).toBeInTheDocument(); }); + test('should hide flapping if the user does not have read access', async () => { + useRuleFormState.mockReturnValue({ + plugins: { + charts: {} as ChartsPluginSetup, + data: {} as DataPublicPluginStart, + dataViews: {} as DataViewsPublicPluginStart, + unifiedSearch: {} as UnifiedSearchPublicPluginStart, + docLinks: {} as DocLinksStart, + application: { + capabilities: { + rulesSettings: { + readFlappingSettingsUI: false, + writeFlappingSettingsUI: true, + }, + }, + }, + }, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + ruleTypeId: '.es-query', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render(); + + expect(screen.queryByTestId('ruleDefinitionFlappingFormGroup')).not.toBeInTheDocument(); + }); + test('should allow flapping to be changed', async () => { useRuleFormState.mockReturnValue({ plugins, @@ -353,9 +412,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], }); @@ -389,9 +450,11 @@ describe('Rule Definition', () => { }, notifyWhen: null, consumer: 'stackAlerts', + ruleTypeId: '.es-query', }, selectedRuleType: ruleType, selectedRuleTypeModel: ruleModel, + availableRuleTypes: [ruleType], canShowConsumerSelection: true, validConsumers: ['logs', 'stackAlerts'], }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx index 3b404edc5d029..997e666e8340f 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { Suspense, useMemo, useState, useCallback } from 'react'; +import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react'; import { EuiEmptyPrompt, EuiLoadingSpinner, @@ -47,7 +47,7 @@ import { RuleAlertDelay } from './rule_alert_delay'; import { RuleConsumerSelection } from './rule_consumer_selection'; import { RuleSchedule } from './rule_schedule'; import { useRuleFormState, useRuleFormDispatch } from '../hooks'; -import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; +import { ALERTING_FEATURE_ID, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; import { getAuthorizedConsumers } from '../utils'; import { RuleSettingsFlappingTitleTooltip } from '../../rule_settings/rule_settings_flapping_title_tooltip'; import { RuleSettingsFlappingForm } from '../../rule_settings/rule_settings_flapping_form'; @@ -62,6 +62,7 @@ export const RuleDefinition = () => { metadata, selectedRuleType, selectedRuleTypeModel, + availableRuleTypes, validConsumers, canShowConsumerSelection = false, flappingSettings, @@ -70,29 +71,44 @@ export const RuleDefinition = () => { const { colorMode } = useEuiTheme(); const dispatch = useRuleFormDispatch(); + useEffect(() => { + // Need to do a dry run validating the params because the Missing Monitor Data rule type + // does not properly initialize the params + if (selectedRuleType.id === 'monitoring_alert_missing_monitoring_data') { + dispatch({ type: 'runValidation' }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const { charts, data, dataViews, unifiedSearch, docLinks, application } = plugins; const { capabilities: { rulesSettings }, } = application; - const { writeFlappingSettingsUI } = rulesSettings || {}; + const { readFlappingSettingsUI, writeFlappingSettingsUI } = rulesSettings || {}; - const { params, schedule, notifyWhen, flapping } = formData; + const { params, schedule, notifyWhen, flapping, consumer, ruleTypeId } = formData; const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState(false); const [isFlappingPopoverOpen, setIsFlappingPopoverOpen] = useState(false); const authorizedConsumers = useMemo(() => { - if (!validConsumers?.length) { + if (consumer !== ALERTING_FEATURE_ID) { + return []; + } + const selectedAvailableRuleType = availableRuleTypes.find((ruleType) => { + return ruleType.id === selectedRuleType.id; + }); + if (!selectedAvailableRuleType?.authorizedConsumers) { return []; } return getAuthorizedConsumers({ - ruleType: selectedRuleType, + ruleType: selectedAvailableRuleType, validConsumers, }); - }, [selectedRuleType, validConsumers]); + }, [consumer, selectedRuleType, availableRuleTypes, validConsumers]); const shouldShowConsumerSelect = useMemo(() => { if (!canShowConsumerSelection) { @@ -107,10 +123,8 @@ export const RuleDefinition = () => { ) { return false; } - return ( - selectedRuleTypeModel.id && MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleTypeModel.id) - ); - }, [authorizedConsumers, selectedRuleTypeModel, canShowConsumerSelection]); + return !!(ruleTypeId && MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleTypeId)); + }, [ruleTypeId, authorizedConsumers, canShowConsumerSelection]); const RuleParamsExpressionComponent = selectedRuleTypeModel.ruleParamsExpression ?? null; @@ -305,8 +319,9 @@ export const RuleDefinition = () => { > - {IS_RULE_SPECIFIC_FLAPPING_ENABLED && ( + {IS_RULE_SPECIFIC_FLAPPING_ENABLED && readFlappingSettingsUI && ( {ALERT_FLAPPING_DETECTION_TITLE}} description={ diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx index 26342d99580a6..1768303c55223 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx @@ -80,9 +80,6 @@ export const RuleSchedule = () => { const onIntervalNumberChange = useCallback( (e: React.ChangeEvent) => { - if (!e.target.validity.valid) { - return; - } const value = e.target.value.trim(); if (INTEGER_REGEX.test(value)) { const parsedValue = parseInt(value, 10); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx index d1a0f6a56fe2b..c09add5ae1c06 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form.tsx @@ -23,11 +23,12 @@ const queryClient = new QueryClient(); export interface RuleFormProps { plugins: RuleFormPlugins; - returnUrl: string; + onCancel?: () => void; + onSubmit?: (ruleId: string) => void; } export const RuleForm = (props: RuleFormProps) => { - const { plugins, returnUrl } = props; + const { plugins, onCancel, onSubmit } = props; const { id, ruleTypeId } = useParams<{ id?: string; ruleTypeId?: string; @@ -35,23 +36,31 @@ export const RuleForm = (props: RuleFormProps) => { const ruleFormComponent = useMemo(() => { if (id) { - return ; + return ; } if (ruleTypeId) { - return ; + return ( + + ); } return ( {RULE_FORM_ROUTE_PARAMS_ERROR_TITLE}} - > - -

{RULE_FORM_ROUTE_PARAMS_ERROR_TEXT}

-
-
+ body={ + +

{RULE_FORM_ROUTE_PARAMS_ERROR_TEXT}

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

{RULE_FORM_CANCEL_MODAL_DESCRIPTION}

+ + )} + ); }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx index 45e2008773583..d937c60aa3a52 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.test.tsx @@ -32,15 +32,27 @@ const onSave = jest.fn(); const onCancel = jest.fn(); hasRuleErrors.mockReturnValue(false); -useRuleFormState.mockReturnValue({ - baseErrors: {}, - paramsErrors: {}, - formData: { - actions: [], - }, -}); describe('rulePageFooter', () => { + beforeEach(() => { + useRuleFormState.mockReturnValue({ + plugins: { + application: { + capabilities: { + actions: { + show: true, + }, + }, + }, + }, + baseErrors: {}, + paramsErrors: {}, + formData: { + actions: [], + }, + }); + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -75,6 +87,30 @@ describe('rulePageFooter', () => { expect(screen.getByTestId('rulePageConfirmCreateRule')).toBeInTheDocument(); }); + test('should not show creat rule confirmation if user cannot read actions', () => { + useRuleFormState.mockReturnValue({ + plugins: { + application: { + capabilities: { + actions: { + show: false, + }, + }, + }, + }, + baseErrors: {}, + paramsErrors: {}, + formData: { + actions: [], + }, + }); + + render(); + fireEvent.click(screen.getByTestId('rulePageFooterSaveButton')); + expect(screen.queryByTestId('rulePageConfirmCreateRule')).not.toBeInTheDocument(); + expect(onSave).toHaveBeenCalled(); + }); + test('should show call onSave if clicking rule confirmation', () => { render(); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx index 09d2ac429fd50..62a0e4b64e4f1 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx @@ -34,6 +34,7 @@ export const RulePageFooter = (props: RulePageFooterProps) => { const { isEdit = false, isSaving = false, onCancel, onSave } = props; const { + plugins: { application }, formData: { actions }, connectors, baseErrors = {}, @@ -78,11 +79,12 @@ export const RulePageFooter = (props: RulePageFooterProps) => { if (isEdit) { return onSave(); } - if (actions.length === 0) { + const canReadConnectors = !!application.capabilities.actions?.show; + if (actions.length === 0 && canReadConnectors) { return setShowCreateConfirmation(true); } onSave(); - }, [actions, isEdit, onSave]); + }, [actions, isEdit, application, onSave]); const onCreateConfirmClick = useCallback(() => { setShowCreateConfirmation(false); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts index 20e87c66f10f4..fca2e30b94434 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts @@ -194,7 +194,7 @@ export const RULE_TYPE_REQUIRED_TEXT = i18n.translate( export const RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT = i18n.translate( 'alertsUIShared.ruleForm.error.belowMinimumAlertDelayText', { - defaultMessage: 'Alert delay must be greater than 1.', + defaultMessage: 'Alert delay must be 1 or greater.', } ); @@ -498,6 +498,34 @@ export const RULE_FORM_RETURN_TITLE = i18n.translate('alertsUIShared.ruleForm.re defaultMessage: 'Return', }); +export const RULE_FORM_CANCEL_MODAL_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalTitle', + { + defaultMessage: 'Discard unsaved changes to rule?', + } +); + +export const RULE_FORM_CANCEL_MODAL_DESCRIPTION = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalDescription', + { + defaultMessage: "You can't recover unsaved changes.", + } +); + +export const RULE_FORM_CANCEL_MODAL_CONFIRM = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalConfirm', + { + defaultMessage: 'Discard changes', + } +); + +export const RULE_FORM_CANCEL_MODAL_CANCEL = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormCancelModalCancel', + { + defaultMessage: 'Cancel', + } +); + export const MODAL_SEARCH_PLACEHOLDER = i18n.translate( 'alertsUIShared.ruleForm.modalSearchPlaceholder', { @@ -586,3 +614,10 @@ export const TECH_PREVIEW_DESCRIPTION = i18n.translate( 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', } ); + +export const DISABLED_ACTIONS_WARNING_TITLE = i18n.translate( + 'alertsUIShared.disabledActionsWarningTitle', + { + defaultMessage: 'This rule has actions that are disabled', + } +); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts index d33c74da528db..4b45f64d3ead4 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts @@ -72,6 +72,7 @@ export interface RuleFormState { connectors: ActionConnector[]; connectorTypes: ActionType[]; aadTemplateFields: ActionVariable[]; + availableRuleTypes: RuleTypeWithDescription[]; baseErrors?: RuleFormBaseErrors; paramsErrors?: RuleFormParamsErrors; actionsErrors?: Record; @@ -83,8 +84,9 @@ export interface RuleFormState { metadata?: Record; minimumScheduleInterval?: MinimumScheduleInterval; canShowConsumerSelection?: boolean; - validConsumers?: RuleCreationValidConsumer[]; + validConsumers: RuleCreationValidConsumer[]; flappingSettings?: RulesSettingsFlapping; + touched?: boolean; } export type InitialRule = Partial & diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts index 217bb18328d0e..0b5234c669440 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts @@ -17,9 +17,6 @@ export const getAuthorizedConsumers = ({ ruleType: RuleTypeWithDescription; validConsumers: RuleCreationValidConsumer[]; }) => { - if (!ruleType.authorizedConsumers) { - return []; - } return Object.entries(ruleType.authorizedConsumers).reduce( (result, [authorizedConsumer, privilege]) => { if ( diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_default_params.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_default_params.ts new file mode 100644 index 0000000000000..d2aab787d6eb5 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_default_params.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", 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 { ActionTypeModel, RuleTypeWithDescription } from '../../common/types'; + +export const getDefaultParams = ({ + group, + ruleType, + actionTypeModel, +}: { + group: string; + actionTypeModel: ActionTypeModel; + ruleType: RuleTypeWithDescription; +}) => { + if (group === ruleType.recoveryActionGroup.id) { + return actionTypeModel.defaultRecoveredActionParams; + } else { + return actionTypeModel.defaultActionParams; + } +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts index f5b583a1a9c63..53c9aedda7545 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts @@ -17,3 +17,4 @@ export * from './get_initial_schedule'; export * from './has_fields_for_aad'; export * from './get_selected_action_group'; export * from './get_initial_consumer'; +export * from './get_default_params'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts b/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts index d65e9c5893937..57afe66b53edf 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/validation/validate_form.ts @@ -35,7 +35,10 @@ export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormAc if ('alertsFilter' in action) { const query = action?.alertsFilter?.query; - if (query && !query.kql) { + if (!query) { + return errors; + } + if (!query.filters.length && !query.kql) { errors.filterQuery.push( i18n.translate('alertsUIShared.ruleForm.actionsForm.requiredFilterQuery', { defaultMessage: 'A custom query is required.', @@ -43,7 +46,6 @@ export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormAc ); } } - return errors; }; @@ -88,11 +90,7 @@ export function validateRuleBase({ errors.ruleTypeId.push(RULE_TYPE_REQUIRED_TEXT); } - if ( - formData.alertDelay && - !isNaN(formData.alertDelay?.active) && - formData.alertDelay?.active < 1 - ) { + if (!formData.alertDelay || isNaN(formData.alertDelay.active) || formData.alertDelay.active < 1) { errors.alertDelay.push(RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT); } @@ -111,34 +109,41 @@ export const validateRuleParams = ({ return ruleTypeModel.validate(formData.params, isServerless).errors; }; -const hasRuleBaseErrors = (errors: RuleFormBaseErrors) => { +export const hasRuleBaseErrors = (errors: RuleFormBaseErrors) => { return Object.values(errors).some((error: string[]) => error.length > 0); }; -const hasActionsError = (actionsErrors: Record) => { +export const hasActionsError = (actionsErrors: Record) => { return Object.values(actionsErrors).some((errors: RuleFormActionsErrors) => { return Object.values(errors).some((error: string[]) => error.length > 0); }); }; -const hasParamsErrors = (errors: RuleFormParamsErrors): boolean => { - const values = Object.values(errors); +export const hasParamsErrors = (errors: RuleFormParamsErrors | string | string[]): boolean => { let hasError = false; - for (const value of values) { - if (Array.isArray(value) && value.length > 0) { - return true; - } - if (typeof value === 'string' && value.trim() !== '') { - return true; - } - if (isObject(value)) { - hasError = hasParamsErrors(value as RuleFormParamsErrors); - } + + if (typeof errors === 'string' && errors.trim() !== '') { + hasError = true; } + + if (Array.isArray(errors)) { + errors.forEach((error) => { + hasError = hasError || hasParamsErrors(error); + }); + } + + if (isObject(errors)) { + Object.entries(errors).forEach(([_, value]) => { + hasError = hasError || hasParamsErrors(value); + }); + } + return hasError; }; -const hasActionsParamsErrors = (actionsParamsErrors: Record) => { +export const hasActionsParamsErrors = ( + actionsParamsErrors: Record +) => { return Object.values(actionsParamsErrors).some((errors: RuleFormParamsErrors) => { return hasParamsErrors(errors); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx index 99f64f0a3977f..030cde8127b0a 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx @@ -218,15 +218,17 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = direction={isDesktop ? 'row' : 'column'} alignItems={isDesktop ? 'center' : undefined} > - + {flappingLabel} - + {enabled ? flappingOnLabel : flappingOffLabel} {flappingSettings && enabled && ( - {flappingOverrideLabel} + + {flappingOverrideLabel} + )} @@ -236,6 +238,7 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = compressed checked={!!flappingSettings} label={flappingOverrideConfiguration} + disabled={!canWriteFlappingSettingsUI} onChange={onFlappingToggle} /> )} @@ -256,6 +259,7 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = spaceFlappingSettings, flappingSettings, flappingOffTooltip, + canWriteFlappingSettingsUI, onFlappingToggle, ]); @@ -273,12 +277,14 @@ export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) = statusChangeThreshold={flappingSettings.statusChangeThreshold} onLookBackWindowChange={onLookBackWindowChange} onStatusChangeThresholdChange={onStatusChangeThresholdChange} + isDisabled={!canWriteFlappingSettingsUI} /> ); }, [ flappingSettings, spaceFlappingSettings, + canWriteFlappingSettingsUI, onLookBackWindowChange, onStatusChangeThresholdChange, ]); diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx index 2a5cc4186013d..149eb5b792c1b 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx @@ -80,6 +80,7 @@ export const RuleSettingsFlappingTitleTooltip = (props: RuleSettingsFlappingTitl panelStyle={{ width: 500, }} + closePopover={() => setIsPopoverOpen(false)} button={ ruleDetailsRoute.replace(':ruleId', ruleId); +export const getCreateRuleRoute = (ruleTypeId: string) => + createRuleRoute.replace(':ruleTypeId', ruleTypeId); +export const getEditRuleRoute = (ruleId: string) => editRuleRoute.replace(':id', ruleId); diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx index 34cb1a4b0ed7a..8cb83176a6591 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx @@ -45,6 +45,7 @@ interface ConnectorConfigurationProps { hasPlatinumLicense: boolean; isLoading: boolean; saveConfig: (configuration: Record) => void; + saveAndSync?: (configuration: Record) => void; stackManagementLink?: string; subscriptionLink?: string; children?: React.ReactNode; @@ -90,6 +91,7 @@ export const ConnectorConfigurationComponent: FC< hasPlatinumLicense, isLoading, saveConfig, + saveAndSync, subscriptionLink, stackManagementLink, }) => { @@ -166,6 +168,12 @@ export const ConnectorConfigurationComponent: FC< saveConfig(config); setIsEditing(false); }} + {...(saveAndSync && { + saveAndSync: (config) => { + saveAndSync(config); + setIsEditing(false); + }, + })} /> ) : ( uncategorizedDisplayList.length > 0 && ( diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx index f7e619f407f12..9b83f7c0d3302 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx @@ -36,6 +36,7 @@ interface ConnectorConfigurationForm { isLoading: boolean; isNative: boolean; saveConfig: (config: Record) => void; + saveAndSync?: (config: Record) => void; stackManagementHref?: string; subscriptionLink?: string; } @@ -60,6 +61,7 @@ export const ConnectorConfigurationForm: React.FC = isLoading, isNative, saveConfig, + saveAndSync, }) => { const [localConfig, setLocalConfig] = useState(configuration); const [configView, setConfigView] = useState( @@ -167,19 +169,7 @@ export const ConnectorConfigurationForm: React.FC = )} - - - - {i18n.translate('searchConnectors.configurationConnector.config.submitButton.title', { - defaultMessage: 'Save configuration', - })} - - + = )} + + + {i18n.translate('searchConnectors.configurationConnector.config.submitButton.title', { + defaultMessage: 'Save', + })} + + + {saveAndSync && ( + + { + saveAndSync(configViewToConfigValues(configView)); + }} + > + {i18n.translate( + 'searchConnectors.configurationConnector.config.submitButton.title', + { + defaultMessage: 'Save and sync', + } + )} + + + )} diff --git a/packages/kbn-telemetry-tools/GUIDELINE.md b/packages/kbn-telemetry-tools/GUIDELINE.md index a22196bb5dc74..d5377cf47b971 100644 --- a/packages/kbn-telemetry-tools/GUIDELINE.md +++ b/packages/kbn-telemetry-tools/GUIDELINE.md @@ -103,7 +103,7 @@ The `--fix` flag will automatically update the persisted json files used by the node scripts/telemetry_check.js --fix ``` -Note that any updates to the stored json files will require a review by the kibana-telemetry team to help us update the telemetry cluster mappings and ensure your changes adhere to our best practices. +Note that any updates to the stored json files will require a review by the kibana-core team to help us update the telemetry cluster mappings and ensure your changes adhere to our best practices. ## Updating the collector schema @@ -116,7 +116,7 @@ Once youre run the changes to both the `fetch` function and the `schema` field r node scripts/telemetry_check.js --fix ``` -The `--fix` flag will automatically update the persisted json files used by the telemetry team. Note that any updates to the stored json files will require a review by the kibana-telemetry team to help us update the telemetry cluster mappings and ensure your changes adhere to our best practices. +The `--fix` flag will automatically update the persisted json files used by the telemetry team. Note that any updates to the stored json files will require a review by the kibana-core team to help us update the telemetry cluster mappings and ensure your changes adhere to our best practices. ## Writing the schema diff --git a/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap b/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap index e58bd37dead6c..787c5e348e51a 100644 --- a/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap +++ b/packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap @@ -2,7 +2,7 @@ exports[` hint element should be tabable 1`] = `
is rendered 1`] = ` onMouseOver={[Function]} >
= ({ role="button" onClick={startEditing} onKeyDown={onKeyDownHint} - aria-label={ariaLabel} + aria-label={i18n.translate('sharedUXPackages.codeEditor.codeEditorEditButton', { + defaultMessage: '{codeEditorAriaLabel}, activate edit mode', + values: { + codeEditorAriaLabel: ariaLabel, + }, + })} data-test-subj={`codeEditorHint codeEditorHint--${isHintActive ? 'active' : 'inactive'}`} /> @@ -528,6 +533,7 @@ export const CodeEditor: React.FC = ({
) : null} + {accessibilityOverlayEnabled && isFullScreen && renderPrompt()} = ({ const useFullScreen = ({ allowFullScreen }: { allowFullScreen?: boolean }) => { const [isFullScreen, setIsFullScreen] = useState(false); + const { euiTheme } = useEuiTheme(); const toggleFullScreen = () => { setIsFullScreen(!isFullScreen); @@ -617,12 +624,12 @@ const useFullScreen = ({ allowFullScreen }: { allowFullScreen?: boolean }) => { return ( -
{children}
+
{children}
); }, - [isFullScreen] + [isFullScreen, euiTheme] ); return { diff --git a/packages/shared-ux/code_editor/impl/editor.styles.ts b/packages/shared-ux/code_editor/impl/editor.styles.ts index 62f15a4a88317..2d12cd01d031b 100644 --- a/packages/shared-ux/code_editor/impl/editor.styles.ts +++ b/packages/shared-ux/code_editor/impl/editor.styles.ts @@ -15,10 +15,11 @@ export const styles = { position: relative; height: 100%; `, - fullscreenContainer: css` + fullscreenContainer: (euiTheme: EuiThemeComputed) => css` position: absolute; left: 0; top: 0; + background: ${euiTheme.colors.body}; `, keyboardHint: (euiTheme: EuiThemeComputed) => css` position: absolute; diff --git a/updatecli-compose.yaml b/updatecli-compose.yaml index 8ad9bd6df8afb..da43161efa6dc 100644 --- a/updatecli-compose.yaml +++ b/updatecli-compose.yaml @@ -2,13 +2,12 @@ # https://www.updatecli.io/docs/core/compose/ policies: - name: Handle ironbank bumps - policy: ghcr.io/elastic/oblt-updatecli-policies/ironbank/templates:0.3.0@sha256:b0c841d8fb294e6b58359462afbc83070dca375ac5dd0c5216c8926872a98bb1 + policy: ghcr.io/elastic/oblt-updatecli-policies/ironbank/templates:0.5.2@sha256:6a237aea2c621a675d644dd51580bd3c0cb4d48591f54f5ba1c2ba88240fa08b values: - .github/updatecli/values.d/scm.yml - .github/updatecli/values.d/ironbank.yml - - name: Update Updatecli policies - policy: ghcr.io/updatecli/policies/autodiscovery/updatecli:0.4.0@sha256:254367f5b1454fd6032b88b314450cd3b6d5e8d5b6c953eb242a6464105eb869 + policy: ghcr.io/updatecli/policies/autodiscovery/updatecli:0.8.0@sha256:99e9e61b501575c2c176c39f2275998d198b590a3f6b1fe829f7315f8d457e7f values: - .github/updatecli/values.d/scm.yml - - .github/updatecli/values.d/updatecli-compose.yml \ No newline at end of file + - .github/updatecli/values.d/updatecli-compose.yml diff --git a/x-pack/examples/triggers_actions_ui_example/public/application.tsx b/x-pack/examples/triggers_actions_ui_example/public/application.tsx index 4a429fbfd58d7..b3c11beb5285c 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/application.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/application.tsx @@ -203,7 +203,6 @@ const TriggersActionsUiExampleApp = ({ ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry, actionTypeRegistry: triggersActionsUi.actionTypeRegistry, }} - returnUrl={application.getUrlForApp('triggersActionsUiExample')} /> )} @@ -229,7 +228,6 @@ const TriggersActionsUiExampleApp = ({ ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry, actionTypeRegistry: triggersActionsUi.actionTypeRegistry, }} - returnUrl={application.getUrlForApp('triggersActionsUiExample')} /> )} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx index 4b1851834cdba..d042a4cfd96f5 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx @@ -15,7 +15,8 @@ import { useLoadConnectors } from '../connectorland/use_load_connectors'; import { DefinedUseQueryResult, UseQueryResult } from '@tanstack/react-query'; -import { useLocalStorage, useSessionStorage } from 'react-use'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import useSessionStorage from 'react-use/lib/useSessionStorage'; import { QuickPrompts } from './quick_prompts/quick_prompts'; import { mockAssistantAvailability, TestProviders } from '../mock/test_providers/test_providers'; import { useFetchCurrentUserConversations } from './api'; @@ -27,7 +28,8 @@ import { omit } from 'lodash'; jest.mock('../connectorland/use_load_connectors'); jest.mock('../connectorland/connector_setup'); -jest.mock('react-use'); +jest.mock('react-use/lib/useLocalStorage'); +jest.mock('react-use/lib/useSessionStorage'); jest.mock('./quick_prompts/quick_prompts', () => ({ QuickPrompts: jest.fn() })); jest.mock('./api/conversations/use_fetch_current_user_conversations'); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx index e46f54ddede40..c3927a939af92 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx @@ -32,15 +32,12 @@ const testTitle = 'SPL_QUERY_CONVERSION_TITLE'; const testPrompt = 'SPL_QUERY_CONVERSION_PROMPT'; const customTitle = 'A_CUSTOM_OPTION'; -jest.mock('react-use', () => ({ - ...jest.requireActual('react-use'), - useMeasure: () => [ - () => {}, - { - width: 500, - }, - ], -})); +jest.mock('react-use/lib/useMeasure', () => () => [ + () => {}, + { + width: 500, + }, +]); jest.mock('../../assistant_context', () => ({ ...jest.requireActual('../../assistant_context'), diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx index 036fb4fb4db3f..f2baf4528b52d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx @@ -14,7 +14,7 @@ import { EuiButtonIcon, EuiButtonEmpty, } from '@elastic/eui'; -import { useMeasure } from 'react-use'; +import useMeasure from 'react-use/lib/useMeasure'; import { css } from '@emotion/react'; import { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx index 5bd49fec6c857..4e877e1886fb4 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx @@ -8,13 +8,11 @@ import { renderHook } from '@testing-library/react-hooks'; import { useAssistantContext } from '.'; -import { useLocalStorage } from 'react-use'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { TestProviders } from '../mock/test_providers/test_providers'; -jest.mock('react-use', () => ({ - useLocalStorage: jest.fn().mockReturnValue(['456', jest.fn()]), - useSessionStorage: jest.fn().mockReturnValue(['456', jest.fn()]), -})); +jest.mock('react-use/lib/useLocalStorage', () => jest.fn().mockReturnValue(['456', jest.fn()])); +jest.mock('react-use/lib/useSessionStorage', () => jest.fn().mockReturnValue(['456', jest.fn()])); describe('AssistantContext', () => { beforeEach(() => jest.clearAllMocks()); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx index 75516eaf907b2..c7b15f681a717 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx @@ -10,7 +10,8 @@ import { omit } from 'lodash/fp'; import React, { useCallback, useMemo, useState, useRef } from 'react'; import type { IToasts } from '@kbn/core-notifications-browser'; import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; -import { useLocalStorage, useSessionStorage } from 'react-use'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import useSessionStorage from 'react-use/lib/useSessionStorage'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { AssistantFeatures, defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; import { NavigateToAppOptions } from '@kbn/core/public'; diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts new file mode 100644 index 0000000000000..8e0fc6ad09479 --- /dev/null +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { joinByKey } from './join_by_key'; + +describe('joinByKey', () => { + it('joins by a string key', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + avg: 10, + }, + { + serviceName: 'opbeans-node', + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + }, + { + serviceName: 'opbeans-java', + p95: 18, + }, + ], + 'serviceName' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + serviceName: 'opbeans-node', + avg: 10, + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + p95: 18, + }, + ]); + }); + + it('joins by a record key', () => { + const joined = joinByKey( + [ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + }, + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + p95: 18, + }, + ], + 'key' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + p95: 18, + }, + ]); + }); + + it('joins by multiple keys', () => { + const data = [ + { + serviceName: 'opbeans-node', + environment: 'production', + type: 'service', + }, + { + serviceName: 'opbeans-node', + environment: 'stage', + type: 'service', + }, + { + serviceName: 'opbeans-node', + hostName: 'host-1', + }, + { + containerId: 'containerId', + }, + ]; + + const alerts = [ + { + serviceName: 'opbeans-node', + environment: 'production', + type: 'service', + alertCount: 10, + }, + { + containerId: 'containerId', + alertCount: 1, + }, + { + hostName: 'host-1', + environment: 'production', + alertCount: 5, + }, + ]; + + const joined = joinByKey( + [...data, ...alerts], + ['serviceName', 'environment', 'hostName', 'containerId'] + ); + + expect(joined.length).toBe(5); + + expect(joined).toEqual([ + { environment: 'stage', serviceName: 'opbeans-node', type: 'service' }, + { hostName: 'host-1', serviceName: 'opbeans-node' }, + { alertCount: 10, environment: 'production', serviceName: 'opbeans-node', type: 'service' }, + { alertCount: 1, containerId: 'containerId' }, + { alertCount: 5, environment: 'production', hostName: 'host-1' }, + ]); + }); + + it('uses the custom merge fn to replace items', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-java', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['b'], + }, + { + serviceName: 'opbeans-node', + values: ['c'], + }, + ], + 'serviceName', + (a, b) => ({ + ...a, + ...b, + values: a.values.concat(b.values), + }) + ); + + expect(joined.find((item) => item.serviceName === 'opbeans-node')?.values).toEqual([ + 'a', + 'b', + 'c', + ]); + }); + + it('deeply merges objects', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + properties: { + foo: '', + }, + }, + { + serviceName: 'opbeans-node', + properties: { + bar: '', + }, + }, + ], + 'serviceName' + ); + + expect(joined[0]).toEqual({ + serviceName: 'opbeans-node', + properties: { + foo: '', + bar: '', + }, + }); + }); +}); diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.ts new file mode 100644 index 0000000000000..54e8ecdaf409b --- /dev/null +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UnionToIntersection, ValuesType } from 'utility-types'; +import { merge, castArray } from 'lodash'; +import stableStringify from 'json-stable-stringify'; + +export type JoinedReturnType< + T extends Record, + U extends UnionToIntersection +> = Array< + Partial & { + [k in keyof T]: T[k]; + } +>; + +type ArrayOrSingle = T | T[]; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends ArrayOrSingle +>(items: T[], key: V): JoinedReturnType; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends ArrayOrSingle, + W extends JoinedReturnType, + X extends (a: T, b: T) => ValuesType +>(items: T[], key: V, mergeFn: X): W; + +export function joinByKey( + items: Array>, + key: string | string[], + mergeFn: Function = (a: Record, b: Record) => merge({}, a, b) +) { + const keys = castArray(key); + // Create a map to quickly query the key of group. + const map = new Map(); + items.forEach((current) => { + // The key of the map is a stable JSON string of the values from given keys. + // We need stable JSON string to support plain object values. + const stableKey = stableStringify(keys.map((k) => current[k])); + + if (map.has(stableKey)) { + const item = map.get(stableKey); + // delete and set the key to put it last + map.delete(stableKey); + map.set(stableKey, mergeFn(item, current)); + } else { + map.set(stableKey, { ...current }); + } + }); + return [...map.values()]; +} diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/list_types/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/list_types/schemas/v1.ts index bc38ef051ed90..5ea3d9219ad35 100644 --- a/x-pack/plugins/alerting/common/routes/rule/apis/list_types/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/apis/list_types/schemas/v1.ts @@ -13,58 +13,175 @@ const actionVariableSchema = schema.object({ usesPublicBaseUrl: schema.maybe(schema.boolean()), }); -const actionGroupSchema = schema.object({ - id: schema.string(), - name: schema.string(), -}); +const actionGroupSchema = schema.object( + { + id: schema.string(), + name: schema.string(), + }, + { + meta: { + description: + 'An action group to use when an alert goes from an active state to an inactive one.', + }, + } +); export const typesRulesResponseBodySchema = schema.arrayOf( schema.object({ - action_groups: schema.maybe(schema.arrayOf(actionGroupSchema)), - action_variables: schema.maybe( - schema.object({ - context: schema.maybe(schema.arrayOf(actionVariableSchema)), - state: schema.maybe(schema.arrayOf(actionVariableSchema)), - params: schema.maybe(schema.arrayOf(actionVariableSchema)), + action_groups: schema.maybe( + schema.arrayOf(actionGroupSchema, { + meta: { + description: + "An explicit list of groups for which the rule type can schedule actions, each with the action group's unique ID and human readable name. Rule actions validation uses this configuration to ensure that groups are valid.", + }, }) ), + action_variables: schema.maybe( + schema.object( + { + context: schema.maybe(schema.arrayOf(actionVariableSchema)), + state: schema.maybe(schema.arrayOf(actionVariableSchema)), + params: schema.maybe(schema.arrayOf(actionVariableSchema)), + }, + { + meta: { + description: + 'A list of action variables that the rule type makes available via context and state in action parameter templates, and a short human readable description. When you create a rule in Kibana, it uses this information to prompt you for these variables in action parameter editors.', + }, + } + ) + ), alerts: schema.maybe( - schema.object({ - context: schema.string(), - mappings: schema.maybe( - schema.object({ - dynamic: schema.maybe(schema.oneOf([schema.literal(false), schema.literal('strict')])), - fieldMap: schema.recordOf(schema.string(), schema.any()), - shouldWrite: schema.maybe(schema.boolean()), - useEcs: schema.maybe(schema.boolean()), - }) - ), - }) + schema.object( + { + context: schema.string({ + meta: { + description: 'The namespace for this rule type.', + }, + }), + mappings: schema.maybe( + schema.object({ + dynamic: schema.maybe( + schema.oneOf([schema.literal(false), schema.literal('strict')], { + meta: { + description: 'Indicates whether new fields are added dynamically.', + }, + }) + ), + fieldMap: schema.recordOf(schema.string(), schema.any(), { + meta: { + description: + 'Mapping information for each field supported in alerts as data documents for this rule type. For more information about mapping parameters, refer to the Elasticsearch documentation.', + }, + }), + shouldWrite: schema.maybe( + schema.boolean({ + meta: { + description: 'Indicates whether the rule should write out alerts as data.', + }, + }) + ), + useEcs: schema.maybe( + schema.boolean({ + meta: { + description: + 'Indicates whether to include the ECS component template for the alerts.', + }, + }) + ), + }) + ), + }, + { + meta: { + description: 'Details for writing alerts as data documents for this rule type.', + }, + } + ) ), authorized_consumers: schema.recordOf( schema.string(), - schema.object({ read: schema.boolean(), all: schema.boolean() }) + schema.object({ read: schema.boolean(), all: schema.boolean() }), + { + meta: { + description: 'The list of the plugins IDs that have access to the rule type.', + }, + } ), - category: schema.string(), - default_action_group_id: schema.string(), + category: schema.string({ + meta: { + description: + 'The rule category, which is used by features such as category-specific maintenance windows.', + }, + }), + default_action_group_id: schema.string({ + meta: { + description: 'The default identifier for the rule type group.', + }, + }), default_schedule_interval: schema.maybe(schema.string()), - does_set_recovery_context: schema.maybe(schema.boolean()), - enabled_in_license: schema.boolean(), + does_set_recovery_context: schema.maybe( + schema.boolean({ + meta: { + description: + 'Indicates whether the rule passes context variables to its recovery action.', + }, + }) + ), + enabled_in_license: schema.boolean({ + meta: { + description: + 'Indicates whether the rule type is enabled or disabled based on the subscription.', + }, + }), fieldsForAAD: schema.maybe(schema.arrayOf(schema.string())), - has_alerts_mappings: schema.boolean(), - has_fields_for_a_a_d: schema.boolean(), - id: schema.string(), - is_exportable: schema.boolean(), - minimum_license_required: schema.oneOf([ - schema.literal('basic'), - schema.literal('gold'), - schema.literal('platinum'), - schema.literal('standard'), - schema.literal('enterprise'), - schema.literal('trial'), - ]), - name: schema.string(), - producer: schema.string(), + has_alerts_mappings: schema.boolean({ + meta: { + description: 'Indicates whether the rule type has custom mappings for the alert data.', + }, + }), + has_fields_for_a_a_d: schema.boolean({ + meta: { + description: + 'Indicates whether the rule type has fields for alert as data for the alert data. ', + }, + }), + id: schema.string({ + meta: { + description: 'The unique identifier for the rule type.', + }, + }), + is_exportable: schema.boolean({ + meta: { + description: + 'Indicates whether the rule type is exportable in Stack Management > Saved Objects.', + }, + }), + minimum_license_required: schema.oneOf( + [ + schema.literal('basic'), + schema.literal('gold'), + schema.literal('platinum'), + schema.literal('standard'), + schema.literal('enterprise'), + schema.literal('trial'), + ], + { + meta: { + description: 'The subscriptions required to use the rule type.', + }, + } + ), + name: schema.string({ + meta: { + description: 'The descriptive name of the rule type.', + }, + }), + producer: schema.string({ + meta: { + description: 'An identifier for the application that produces this rule type.', + }, + }), recovery_action_group: actionGroupSchema, rule_task_timeout: schema.maybe(schema.string()), }) diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/list_types/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule/apis/list_types/rule_types.ts index d6f2ffbe9af0c..da9c62ab5f3f2 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/list_types/rule_types.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/list_types/rule_types.ts @@ -6,7 +6,10 @@ */ import { IRouter } from '@kbn/core/server'; -import { TypesRulesResponseBodyV1 } from '../../../../../common/routes/rule/apis/list_types'; +import { + TypesRulesResponseBodyV1, + typesRulesResponseSchemaV1, +} from '../../../../../common/routes/rule/apis/list_types'; import { ILicenseState } from '../../../../lib'; import { verifyAccessAndContext } from '../../../lib'; import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; @@ -24,7 +27,18 @@ export const ruleTypesRoute = ( summary: `Get the rule types`, tags: ['oas-tag:alerting'], }, - validate: {}, + validate: { + request: {}, + response: { + 200: { + body: () => typesRulesResponseSchemaV1, + description: 'Indicates a successful call.', + }, + 401: { + description: 'Authorization information is missing or invalid.', + }, + }, + }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/mute_all/mute_all_rule.ts b/x-pack/plugins/alerting/server/routes/rule/apis/mute_all/mute_all_rule.ts index 8ac77973575bb..e9aa0e42a046f 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/mute_all/mute_all_rule.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/mute_all/mute_all_rule.ts @@ -37,6 +37,15 @@ export const muteAllRuleRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_all/unmute_all_rule.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_all/unmute_all_rule.ts index f9ab7d8d8d284..8409128da6241 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_all/unmute_all_rule.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_all/unmute_all_rule.ts @@ -34,6 +34,15 @@ export const unmuteAllRuleRoute = ( 204: { description: 'Indicates a successful call.', }, + 400: { + description: 'Indicates an invalid schema or parameters.', + }, + 403: { + description: 'Indicates that this call is forbidden.', + }, + 404: { + description: 'Indicates a rule with the given ID does not exist.', + }, }, }, }, diff --git a/x-pack/plugins/cases/common/types/domain/user/v1.test.ts b/x-pack/plugins/cases/common/types/domain/user/v1.test.ts index 56d23fff6fc1a..3c90054857e93 100644 --- a/x-pack/plugins/cases/common/types/domain/user/v1.test.ts +++ b/x-pack/plugins/cases/common/types/domain/user/v1.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { UserRt, UserWithProfileInfoRt, UsersRt, CaseUserProfileRt, CaseAssigneesRt } from './v1'; describe('User', () => { diff --git a/x-pack/plugins/cases/public/components/visualizations/open_lens_button.test.tsx b/x-pack/plugins/cases/public/components/visualizations/open_lens_button.test.tsx index 7ac2ed8d45da4..752bdd2980987 100644 --- a/x-pack/plugins/cases/public/components/visualizations/open_lens_button.test.tsx +++ b/x-pack/plugins/cases/public/components/visualizations/open_lens_button.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import React from 'react'; import { screen } from '@testing-library/react'; import type { AppMockRenderer } from '../../common/mock'; diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts b/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts index ea64b20f2c1a2..4d5d167a58852 100644 --- a/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts @@ -12,7 +12,8 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { CasesOracleService } from './cases_oracle_service'; import { CASE_RULES_SAVED_OBJECT } from '../../../common/constants'; -import { isEmpty, set } from 'lodash'; +import { isEmpty } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; describe('CasesOracleService', () => { const savedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_service.test.ts b/x-pack/plugins/cases/server/connectors/cases/cases_service.test.ts index 848d3fa276236..183d628d7a742 100644 --- a/x-pack/plugins/cases/server/connectors/cases/cases_service.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/cases_service.test.ts @@ -8,7 +8,8 @@ import { createHash } from 'node:crypto'; import stringify from 'json-stable-stringify'; -import { isEmpty, set } from 'lodash'; +import { isEmpty } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { CasesService } from './cases_service'; describe('CasesService', () => { diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts index 20c06f2701fed..9e5b7589f1626 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { set, omit, unset } from 'lodash'; +import { omit, unset } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { loggerMock } from '@kbn/logging-mocks'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import type { diff --git a/x-pack/plugins/cases/server/services/user_actions/operations/create.test.ts b/x-pack/plugins/cases/server/services/user_actions/operations/create.test.ts index 833e8676a2619..38fb3e4e746ec 100644 --- a/x-pack/plugins/cases/server/services/user_actions/operations/create.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/operations/create.test.ts @@ -11,7 +11,8 @@ import { createSavedObjectsSerializerMock } from '../../../client/mocks'; import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { set, unset } from 'lodash'; +import { unset } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { createConnectorObject } from '../../test_utils'; import { UserActionPersister } from './create'; import { createUserActionSO } from '../test_utils'; diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 795237ef9b427..4da0244b2ec5e 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import dedent from 'dedent'; + import { ENTERPRISE_SEARCH_APP_ID, ENTERPRISE_SEARCH_CONTENT_APP_ID, @@ -210,6 +212,58 @@ export const SEARCH_RELEVANCE_PLUGIN = { SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/', }; +export const CREATE_CONNECTOR_PLUGIN = { + CLI_SNIPPET: dedent`./bin/connectors connector create + --index-name my-index + --index-language en + --from-file config.yml + `, + CONSOLE_SNIPPET: dedent`# Create an index +PUT /my-index-000001 +{ + "settings": { + "index": { + "number_of_shards": 3, + "number_of_replicas": 2 + } + } +} + +# Create an API key +POST /_security/api_key +{ + "name": "my-api-key", + "expiration": "1d", + "role_descriptors": + { + "role-a": { + "cluster": ["all"], + "indices": [ + { + "names": ["index-a*"], + "privileges": ["read"] + } + ] + }, + "role-b": { + "cluster": ["all"], + "indices": [ + { + "names": ["index-b*"], + "privileges": ["all"] + }] + } + }, "metadata": + { "application": "my-application", + "environment": { + "level": 1, + "trusted": true, + "tags": ["dev", "staging"] + } + } + }`, +}; + export const LICENSED_SUPPORT_URL = 'https://support.elastic.co'; export const JSON_HEADER = { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts index be8e23bdca1c5..3593a7b123533 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/add_connector_api_logic.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; interface AddConnectorValue { @@ -20,11 +20,17 @@ export interface AddConnectorApiLogicArgs { language: string | null; name: string; serviceType?: string; + // Without a proper refactoring there is no good way to chain actions. + // This prop is simply passed back with the result to let listeners + // know what was the intent of the request. And call the next action + // accordingly. + uiFlags?: Record; } export interface AddConnectorApiLogicResponse { id: string; indexName: string; + uiFlags?: Record; } export const addConnector = async ({ @@ -34,6 +40,7 @@ export const addConnector = async ({ isNative, language, serviceType, + uiFlags, }: AddConnectorApiLogicArgs): Promise => { const route = '/internal/enterprise_search/connectors'; @@ -54,7 +61,12 @@ export const addConnector = async ({ return { id: result.id, indexName: result.index_name, + uiFlags, }; }; export const AddConnectorApiLogic = createApiLogic(['add_connector_api_logic'], addConnector); +export type AddConnectorApiLogicActions = Actions< + AddConnectorApiLogicArgs, + AddConnectorApiLogicResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts index 21edf734bc230..449d3f6628648 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_config_api_logic.ts @@ -5,13 +5,15 @@ * 2.0. */ -import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; export interface GenerateConfigApiArgs { connectorId: string; } +export type GenerateConfigApiActions = Actions; + export const generateConnectorConfig = async ({ connectorId }: GenerateConfigApiArgs) => { const route = `/internal/enterprise_search/connectors/${connectorId}/generate_config`; return await HttpLogic.values.http.post(route); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts index 5583c8c8e22e4..8d2ee0ee87aa3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts @@ -4,23 +4,38 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../shared/http'; export interface GenerateConnectorNamesApiArgs { + connectorName?: string; connectorType?: string; } +export interface GenerateConnectorNamesApiResponse { + apiKeyName: string; + connectorName: string; + indexName: string; +} + export const generateConnectorNames = async ( - { connectorType }: GenerateConnectorNamesApiArgs = { connectorType: 'custom' } + { connectorType, connectorName }: GenerateConnectorNamesApiArgs = { connectorType: 'custom' } ) => { + if (connectorType === '') { + connectorType = 'custom'; + } const route = `/internal/enterprise_search/connectors/generate_connector_name`; return await HttpLogic.values.http.post(route, { - body: JSON.stringify({ connectorType }), + body: JSON.stringify({ connectorName, connectorType }), }); }; export const GenerateConnectorNamesApiLogic = createApiLogic( - ['generate_config_api_logic'], + ['generate_connector_names_api_logic'], generateConnectorNames ); + +export type GenerateConnectorNamesApiLogicActions = Actions< + GenerateConnectorNamesApiArgs, + GenerateConnectorNamesApiResponse +>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx index bb34d652ee74d..ed28ba575d824 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/components/generate_config_button.tsx @@ -12,13 +12,15 @@ import { i18n } from '@kbn/i18n'; export interface GenerateConfigButtonProps { connectorId: string; + disabled?: boolean; generateConfiguration: (params: { connectorId: string }) => void; isGenerateLoading: boolean; } export const GenerateConfigButton: React.FC = ({ connectorId, + disabled, generateConfiguration, - isGenerateLoading, + isGenerateLoading = false, }) => { return ( @@ -26,6 +28,7 @@ export const GenerateConfigButton: React.FC = ({ void; + generateApiKey?: () => void; isGenerateLoading: boolean; } @@ -93,7 +93,7 @@ export const GeneratedConfigFields: React.FC = ({ }; const onConfirm = () => { - generateApiKey(); + if (generateApiKey) generateApiKey(); setIsModalVisible(false); }; @@ -222,16 +222,18 @@ export const GeneratedConfigFields: React.FC = ({ {apiKey?.encoded} - - - + {generateApiKey && ( + + + + )} = ({ ) : ( - - - + generateApiKey && ( + + + + ) )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx index 2c20902793093..e3bd0e867af3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx @@ -61,6 +61,22 @@ export const ConnectorDeployment: React.FC = () => { Record >('search:connector-ui-options', {}); + useEffect(() => { + if (connectorId && connector && connector.api_key_id) { + getApiKeyById(connector.api_key_id); + } + }, [connector, connectorId]); + + const selectDeploymentMethod = (deploymentMethod: 'docker' | 'source') => { + if (connector) { + setSelectedDeploymentMethod(deploymentMethod); + setConnectorUiOptions({ + ...connectorUiOptions, + [connector.id]: { deploymentMethod }, + }); + } + }; + useEffect(() => { if (connectorUiOptions && connectorId && connectorUiOptions[connectorId]) { setSelectedDeploymentMethod(connectorUiOptions[connectorId].deploymentMethod); @@ -68,25 +84,10 @@ export const ConnectorDeployment: React.FC = () => { selectDeploymentMethod('docker'); } }, [connectorUiOptions, connectorId]); - - useEffect(() => { - if (connectorId && connector && connector.api_key_id) { - getApiKeyById(connector.api_key_id); - } - }, [connector, connectorId]); - if (!connector || connector.is_native) { return <>; } - const selectDeploymentMethod = (deploymentMethod: 'docker' | 'source') => { - setSelectedDeploymentMethod(deploymentMethod); - setConnectorUiOptions({ - ...connectorUiOptions, - [connector.id]: { deploymentMethod }, - }); - }; - const hasApiKey = !!(connector.api_key_id ?? generatedData?.apiKey); const isWaitingForConnector = !connector.status || connector.status === ConnectorStatus.CREATED; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts index 09c2c8db48e03..13f3cc0b30369 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment_logic.ts @@ -10,15 +10,12 @@ import { kea, MakeLogicType } from 'kea'; import { Connector } from '@kbn/search-connectors'; import { HttpError, Status } from '../../../../../common/types/api'; -import { Actions } from '../../../shared/api_logic/create_api_logic'; import { - GenerateConfigApiArgs, + GenerateConfigApiActions, GenerateConfigApiLogic, } from '../../api/connector/generate_connector_config_api_logic'; import { APIKeyResponse } from '../../api/generate_api_key/generate_api_key_logic'; -type GenerateConfigApiActions = Actions; - export interface DeploymentLogicValues { generateConfigurationError: HttpError; generateConfigurationStatus: Status; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx index a29f6c540b7ce..c12dd8036b6b9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx @@ -44,8 +44,8 @@ import { ConnectorStats } from './connector_stats'; import { ConnectorsLogic } from './connectors_logic'; import { ConnectorsTable } from './connectors_table'; import { CrawlerEmptyState } from './crawler_empty_state'; +import { CreateConnector } from './create_connector'; import { DeleteConnectorModal } from './delete_connector_modal'; -import { SelectConnector } from './select_connector/select_connector'; export const connectorsBreadcrumbs = [ i18n.translate('xpack.enterpriseSearch.content.connectors.breadcrumb', { @@ -81,7 +81,7 @@ export const Connectors: React.FC = ({ isCrawler }) => { }, [searchParams.from, searchParams.size, searchQuery, isCrawler]); return !isLoading && isEmpty && !isCrawler ? ( - + ) : ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx index dc5ed0342c3be..9020a1d165168 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_router.tsx @@ -13,23 +13,27 @@ import { CONNECTORS_PATH, NEW_INDEX_SELECT_CONNECTOR_PATH, NEW_CONNECTOR_PATH, + NEW_CONNECTOR_FLOW_PATH, CONNECTOR_DETAIL_PATH, } from '../../routes'; import { ConnectorDetailRouter } from '../connector_detail/connector_detail_router'; import { NewSearchIndexPage } from '../new_index/new_search_index_page'; import { Connectors } from './connectors'; -import { SelectConnector } from './select_connector/select_connector'; +import { CreateConnector } from './create_connector'; export const ConnectorsRouter: React.FC = () => { return ( - + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logo.svg b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logo.svg new file mode 100644 index 0000000000000..f827c8dce36eb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logos_comp.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logos_comp.png new file mode 100644 index 0000000000000..22f5ad4c31a31 Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/assets/connector_logos_comp.png differ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx new file mode 100644 index 0000000000000..6c5505a22f81e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/choose_connector_selectable.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiBadge, + EuiFlexItem, + EuiIcon, + EuiInputPopover, + EuiSelectable, + EuiSelectableOption, + useEuiTheme, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { KibanaLogic } from '../../../../../shared/kibana'; +import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic'; +import { SelfManagePreference } from '../create_connector'; + +interface ChooseConnectorSelectableProps { + selfManaged: SelfManagePreference; +} +interface OptionData { + secondaryContent?: string; +} + +export const ChooseConnectorSelectable: React.FC = ({ + selfManaged, +}) => { + const { euiTheme } = useEuiTheme(); + const [isOpen, setIsOpen] = useState(false); + const [selectableOptions, selectableSetOptions] = useState< + Array> + >([]); + const { connectorTypes } = useValues(KibanaLogic); + const allConnectors = useMemo( + () => connectorTypes.sort((a, b) => a.name.localeCompare(b.name)), + [connectorTypes] + ); + const { selectedConnector } = useValues(NewConnectorLogic); + const { setSelectedConnector } = useActions(NewConnectorLogic); + + const getInitialOptions = () => { + return allConnectors.map((connector, key) => { + const append: JSX.Element[] = []; + if (connector.isTechPreview) { + append.push( + + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel', + { defaultMessage: 'Tech preview' } + )} + + ); + } + if (connector.isBeta) { + append.push( + + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel', + { + defaultMessage: 'Beta', + } + )} + + ); + } + if (selfManaged === 'native' && !connector.isNative) { + append.push( + + {i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel', + { + defaultMessage: 'Self managed', + } + )} + + ); + } + + return { + append, + key: key.toString(), + label: connector.name, + prepend: , + }; + }); + }; + + const initialOptions = getInitialOptions(); + + useEffect(() => { + selectableSetOptions(initialOptions); + }, [selfManaged]); + const [searchValue, setSearchValue] = useState(''); + + const openPopover = useCallback(() => { + setIsOpen(true); + }, []); + const closePopover = useCallback(() => { + setIsOpen(false); + }, []); + + return ( + + { + selectableSetOptions(newOptions); + closePopover(); + if (changedOption.checked === 'on') { + const keySelected = Number(changedOption.key); + setSelectedConnector(allConnectors[keySelected]); + setSearchValue(allConnectors[keySelected].name); + } else { + setSelectedConnector(null); + setSearchValue(''); + } + }} + listProps={{ + isVirtualized: true, + rowHeight: Number(euiTheme.base * 3), + showIcons: false, + }} + singleSelection + searchable + searchProps={{ + fullWidth: true, + isClearable: true, + onChange: (value) => { + if (value !== selectedConnector?.name) { + setSearchValue(value); + } + }, + onClick: openPopover, + onFocus: openPopover, + placeholder: i18n.translate( + 'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text', + { defaultMessage: 'Choose a data source' } + ), + value: searchValue, + }} + > + {(list, search) => ( + + {list} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/connector_description_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/connector_description_popover.tsx new file mode 100644 index 0000000000000..b19a5ac8ddba5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/connector_description_popover.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import connectorLogo from '../../../../../../assets/images/connector_logo_network_drive_version.svg'; + +const nativePopoverPanels = [ + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.native.chooseADataSourceLabel', + { defaultMessage: 'Choose a data source you would like to sync' } + ), + icons: [], + id: 'native-choose-source', + }, + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.native.configureConnectorLabel', + { defaultMessage: 'Configure your connector using our Kibana UI' } + ), + icons: [, ], + id: 'native-configure-connector', + }, +]; + +const connectorClientPopoverPanels = [ + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.chooseADataSourceLabel', + { defaultMessage: 'Choose a data source you would like to sync' } + ), + icons: [], + id: 'client-choose-source', + }, + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.configureConnectorLabel', + { + defaultMessage: + 'Deploy connector code on your own infrastructure by running from source or using Docker', + } + ), + icons: [ + , + , + , + ], + id: 'client-deploy', + }, + { + description: i18n.translate( + 'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.enterDetailsLabel', + { + defaultMessage: 'Enter access and connection details for your data source', + } + ), + icons: [ + , + , + , + , + , + ], + id: 'client-configure-connector', + }, +]; + +export interface ConnectorDescriptionPopoverProps { + isDisabled: boolean; + isNative: boolean; +} + +export const ConnectorDescriptionPopover: React.FC = ({ + isNative, + isDisabled, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const panels = isNative ? nativePopoverPanels : connectorClientPopoverPanels; + return ( + setIsPopoverOpen(!isPopoverOpen)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => { + setIsPopoverOpen(false); + }} + > + + {isDisabled && ( + + + + + + )} + + + {panels.map((panel) => { + return ( + + + + + {panel.icons.map((icon, index) => ( + + {icon} + + ))} + + + + +

{panel.description}

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

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

+
+ + +

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

+
+
+ + + + +

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

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

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

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

{CLI_LABEL}

+
+
+ + +

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

+
+ + + {CREATE_CONNECTOR_PLUGIN.CLI_SNIPPET} + +
+ + )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx new file mode 100644 index 0000000000000..8644cd72f53d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, + EuiText, + EuiButton, + EuiProgress, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ConnectorConfigurationComponent, ConnectorStatus } from '@kbn/search-connectors'; + +import { Status } from '../../../../../../common/types/api'; + +import * as Constants from '../../../../shared/constants'; +import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic'; +import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic'; + +interface ConfigurationStepProps { + setCurrentStep: Function; + title: string; +} + +export const ConfigurationStep: React.FC = ({ title, setCurrentStep }) => { + const { connector } = useValues(ConnectorViewLogic); + const { updateConnectorConfiguration } = useActions(ConnectorViewLogic); + const { status } = useValues(ConnectorConfigurationApiLogic); + const isSyncing = false; + + const isNextStepEnabled = + connector?.status === ConnectorStatus.CONNECTED || + connector?.status === ConnectorStatus.CONFIGURED; + + useEffect(() => { + setTimeout(() => { + window.scrollTo({ + behavior: 'smooth', + top: 0, + }); + }, 100); + }, []); + + if (!connector) return null; + + return ( + <> + + + + +

{title}

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

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

+
+ + +

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

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

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

+
+ + + )} + {currentStep !== 'start' && ( + <> + + + + {selectedConnector?.name} + + ), + value: 'item1', + }, + ]} + /> + + + + {selfManagePreference + ? i18n.translate( + 'xpack.enterpriseSearch.createConnector.badgeType.selfManaged', + { + defaultMessage: 'Self managed', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.createConnector.badgeType.ElasticManaged', + { + defaultMessage: 'Elastic managed', + } + )} + + + )} +
+
+ {/* Col 2 */} + {stepContent[currentStep]} +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/deployment_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/deployment_step.tsx new file mode 100644 index 0000000000000..6e5245f072b4b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/deployment_step.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useValues } from 'kea'; + +import { EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiButton, EuiFlexGroup } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ConnectorStatus } from '@kbn/search-connectors'; + +import * as Constants from '../../../../shared/constants'; +import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic'; +import { ConnectorDeployment } from '../../connector_detail/deployment'; + +interface DeploymentStepProps { + setCurrentStep: Function; +} + +export const DeploymentStep: React.FC = ({ setCurrentStep }) => { + const { connector } = useValues(ConnectorViewLogic); + const isNextStepEnabled = + connector && !(!connector.status || connector.status === ConnectorStatus.CREATED); + + useEffect(() => { + setTimeout(() => { + window.scrollTo({ + behavior: 'smooth', + top: 0, + }); + }, 100); + }, []); + return ( + + + + + +

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

+
+ + +

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

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

{title}

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

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

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

{title}

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

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

+
+ + +

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

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

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

+
+ + +

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

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

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

+
+ + +

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

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

+

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

+

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

+

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

+

{i18n.translate('xpack.enterpriseSearch.apiKey.metadata.title', { defaultMessage: 'Metadata', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts index f163158462f0d..fc9860e202130 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts @@ -49,6 +49,10 @@ export const BACK_BUTTON_LABEL = i18n.translate('xpack.enterpriseSearch.actions. defaultMessage: 'Back', }); +export const NEXT_BUTTON_LABEL = i18n.translate('xpack.enterpriseSearch.actions.nextButtonLabel', { + defaultMessage: 'Next', +}); + export const CLOSE_BUTTON_LABEL = i18n.translate( 'xpack.enterpriseSearch.actions.closeButtonLabel', { defaultMessage: 'Close' } diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts index f6c209707a8f7..56f849c551400 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/generate_connector_name.ts @@ -16,24 +16,51 @@ import { indexOrAliasExists } from '../indices/exists_index'; export const generateConnectorName = async ( client: IScopedClusterClient, - connectorType: string + connectorType: string, + userConnectorName?: string ): Promise<{ apiKeyName: string; connectorName: string; indexName: string }> => { const prefix = toAlphanumeric(connectorType); if (!prefix || prefix.length === 0) { - throw new Error('Connector type is required'); + throw new Error('Connector type or connectorName is required'); } - for (let i = 0; i < 20; i++) { - const connectorName = `${prefix}-${uuidv4().split('-')[1]}`; - const indexName = `connector-${connectorName}`; - - const result = await indexOrAliasExists(client, indexName); - if (!result) { + if (userConnectorName) { + let indexName = `connector-${userConnectorName}`; + const resultSameName = await indexOrAliasExists(client, indexName); + // index with same name doesn't exist + if (!resultSameName) { return { - apiKeyName: indexName, - connectorName, + apiKeyName: userConnectorName, + connectorName: userConnectorName, indexName, }; } + // if the index name already exists, we will generate until it doesn't for 20 times + for (let i = 0; i < 20; i++) { + indexName = `connector-${userConnectorName}-${uuidv4().split('-')[1].slice(0, 4)}`; + + const result = await indexOrAliasExists(client, indexName); + if (!result) { + return { + apiKeyName: indexName, + connectorName: userConnectorName, + indexName, + }; + } + } + } else { + for (let i = 0; i < 20; i++) { + const connectorName = `${prefix}-${uuidv4().split('-')[1].slice(0, 4)}`; + const indexName = `connector-${connectorName}`; + + const result = await indexOrAliasExists(client, indexName); + if (!result) { + return { + apiKeyName: indexName, + connectorName, + indexName, + }; + } + } } throw new Error(ErrorCode.GENERATE_INDEX_NAME_ERROR); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts index 21b00e82b6aa0..6108580463893 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -6,7 +6,6 @@ */ import { schema } from '@kbn/config-schema'; - import { ElasticsearchErrorDetails } from '@kbn/es-errors'; import { i18n } from '@kbn/i18n'; @@ -841,15 +840,20 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { path: '/internal/enterprise_search/connectors/generate_connector_name', validate: { body: schema.object({ + connectorName: schema.maybe(schema.string()), connectorType: schema.string(), }), }, }, elasticsearchErrorHandler(log, async (context, request, response) => { const { client } = (await context.core).elasticsearch; - const { connectorType } = request.body; + const { connectorType, connectorName } = request.body; try { - const generatedNames = await generateConnectorName(client, connectorType ?? 'custom'); + const generatedNames = await generateConnectorName( + client, + connectorType ?? 'custom', + connectorName + ); return response.ok({ body: generatedNames, headers: { 'content-type': 'application/json' }, diff --git a/x-pack/plugins/integration_assistant/public/common/hooks/use_availability.ts b/x-pack/plugins/integration_assistant/public/common/hooks/use_availability.ts index 02f523fcde226..3fdf37297ad65 100644 --- a/x-pack/plugins/integration_assistant/public/common/hooks/use_availability.ts +++ b/x-pack/plugins/integration_assistant/public/common/hooks/use_availability.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { MINIMUM_LICENSE_TYPE } from '../../../common/constants'; import { useKibana } from './use_kibana'; import type { RenderUpselling } from '../../services'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration.tsx index 494bc94d8c58c..6afacc8e417f3 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration.tsx @@ -5,8 +5,8 @@ * 2.0. */ import React from 'react'; -import { Redirect, Switch } from 'react-router-dom'; -import { Route } from '@kbn/shared-ux-router'; +import { Redirect } from 'react-router-dom'; +import { Route, Routes } from '@kbn/shared-ux-router'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { Services } from '../../services'; import { TelemetryContextProvider } from './telemetry'; @@ -33,7 +33,7 @@ const CreateIntegrationRouter = React.memo(() => { const { canUseIntegrationAssistant, canUseIntegrationUpload } = useRoutesAuthorization(); const isAvailable = useIsAvailable(); return ( - + {isAvailable && canUseIntegrationAssistant && ( )} @@ -44,7 +44,7 @@ const CreateIntegrationRouter = React.memo(() => { } /> - + ); }); CreateIntegrationRouter.displayName = 'CreateIntegrationRouter'; diff --git a/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/detection.ts b/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/detection.ts index a8334432a0211..c0172f2d139d0 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/detection.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/detection.ts @@ -26,7 +26,7 @@ export async function handleLogFormatDetection({ const logFormatDetectionResult = await logFormatDetectionNode.invoke({ ex_answer: state.exAnswer, - log_samples: samples, + log_samples: samples.join('\n'), package_title: state.packageTitle, datastream_title: state.dataStreamTitle, }); diff --git a/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/prompts.ts b/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/prompts.ts index 71246d46363cb..b6e777a87888a 100644 --- a/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/prompts.ts +++ b/x-pack/plugins/integration_assistant/server/graphs/log_type_detection/prompts.ts @@ -17,16 +17,18 @@ export const LOG_FORMAT_DETECTION_PROMPT = ChatPromptTemplate.fromMessages([ The samples apply to the data stream {datastream_title} inside the integration package {package_title}. Follow these steps to do this: -1. Go through each log sample and identify the log format. Output this as "name: ". -2. If the samples have any or all of priority, timestamp, loglevel, hostname, ipAddress, messageId in the beginning information then set "header: true". -3. If the samples have a syslog header then set "header: true" , else set "header: false". If you are unable to determine the syslog header presence then set "header: false". -4. If the log samples have structured message body with key-value pairs then classify it as "name: structured". Look for a flat list of key-value pairs, often separated by spaces, commas, or other delimiters. -5. Consider variations in formatting, such as quotes around values ("key=value", key="value"), special characters in keys or values, or escape sequences. -6. If the log samples have unstructured body like a free-form text then classify it as "name: unstructured". -7. If the log samples follow a csv format then classify it with "name: csv". There are two sub-cases for csv: - a. If there is a csv header then set "header: true". - b. If there is no csv header then set "header: false" and try to find good names for the columns in the "columns" array by looking into the values of data in those columns. For each column, if you are unable to find good name candidate for it then output an empty string, like in the example. -8. If you cannot put the format into any of the above categories then classify it with "name: unsupported". +1. Go through each log sample and identify the log format. Output this as "name: ". Here are the values for log_format: + * 'csv': If the log samples follow a Comma-Separated Values format then classify it with "name: csv". There are two sub-cases for csv: + a. If there is a csv header then set "header: true". + b. If there is no csv header then set "header: false" and try to find good names for the columns in the "columns" array by looking into the values of data in those columns. For each column, if you are unable to find good name candidate for it then output an empty string, like in the example. + * 'structured': If the log samples have structured message body with key-value pairs then classify it as "name: structured". Look for a flat list of key-value pairs, often separated by some delimiters. Consider variations in formatting, such as quotes around values ("key=value", key="value"), special characters in keys or values, or escape sequences. + * 'unstructured': If the log samples have unstructured body like a free-form text then classify it as "name: unstructured". + * 'unsupported': If you cannot put the format into any of the above categories then classify it with "name: unsupported". +2. Header: for structured and unstructured format: + - if the samples have any or all of priority, timestamp, loglevel, hostname, ipAddress, messageId in the beginning information then set "header: true". + - if the samples have a syslog header then set "header: true" + - else set "header: false". If you are unable to determine the syslog header presence then set "header: false". +3. Note that a comma-separated list should be classified as 'csv' if its rows only contain values separated by commas. But if it looks like a list of comma separated key-values pairs like 'key1=value1, key2=value2' it should be classified as 'structured'. You ALWAYS follow these guidelines when writing your response: diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 40fae48cb9dc3..7df71559aa97a 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -6,6 +6,9 @@ */ import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema'; import { + HOST_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, AGENT_NAME, CLOUD_PROVIDER, CONTAINER_ID, @@ -15,9 +18,6 @@ import { ENTITY_IDENTITY_FIELDS, ENTITY_LAST_SEEN, ENTITY_TYPE, - HOST_NAME, - SERVICE_ENVIRONMENT, - SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; @@ -28,8 +28,19 @@ export const entityTypeRt = t.union([ t.literal('container'), ]); +export const entityColumnIdsRt = t.union([ + t.literal(ENTITY_DISPLAY_NAME), + t.literal(ENTITY_LAST_SEEN), + t.literal(ENTITY_TYPE), + t.literal('alertsCount'), +]); + +export type EntityColumnIds = t.TypeOf; + export type EntityType = t.TypeOf; +export const defaultEntitySortField: EntityColumnIds = 'alertsCount'; + export const MAX_NUMBER_OF_ENTITIES = 500; export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ @@ -79,6 +90,7 @@ interface BaseEntity { [ENTITY_DISPLAY_NAME]: string; [ENTITY_DEFINITION_ID]: string; [ENTITY_IDENTITY_FIELDS]: string | string[]; + alertsCount?: number; [key: string]: any; } diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts new file mode 100644 index 0000000000000..c4b48410456f8 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '@kbn/observability-shared-plugin/common'; +import { HostEntity, ServiceEntity } from '../entities'; +import { parseIdentityFieldValuesToKql } from './parse_identity_field_values_to_kql'; + +const commonEntityFields = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + alertCount: 3, +}; + +describe('parseIdentityFieldValuesToKql', () => { + it('should return the value when identityFields is a single string', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': 'service.name', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual('service.name: "my-service"'); + }); + + it('should return values when identityFields is an array of strings', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'my-service', + 'entity.type': 'service', + 'service.environment': 'staging', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual('service.name: "my-service" AND service.environment: "staging"'); + }); + + it('should return an empty string if identityFields is empty string', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': '', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual(''); + }); + it('should return an empty array if identityFields is empty array', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': [], + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual(''); + }); + + it('should ignore fields that are not present in the entity', () => { + const entity: HostEntity = { + 'entity.identityFields': ['host.name', 'foo.bar'], + 'host.name': 'my-host', + 'entity.type': 'host', + 'cloud.provider': null, + ...commonEntityFields, + }; + + const result = parseIdentityFieldValuesToKql({ entity }); + expect(result).toEqual('host.name: "my-host"'); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts new file mode 100644 index 0000000000000..2e3f3dadd4109 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ENTITY_IDENTITY_FIELDS } from '@kbn/observability-shared-plugin/common'; +import { Entity } from '../entities'; + +type Operator = 'AND'; +export function parseIdentityFieldValuesToKql({ + entity, + operator = 'AND', +}: { + entity: Entity; + operator?: Operator; +}) { + const mapping: string[] = []; + + const identityFields = entity[ENTITY_IDENTITY_FIELDS]; + + if (identityFields) { + const fields = [identityFields].flat(); + + fields.forEach((field) => { + if (field in entity) { + mapping.push(`${[field]}: "${entity[field as keyof Entity]}"`); + } + }); + } + + return mapping.join(` ${operator} `); +} diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc index 1467d294a4f49..fc77163ae3c5f 100644 --- a/x-pack/plugins/observability_solution/inventory/kibana.jsonc +++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc @@ -16,6 +16,7 @@ "features", "unifiedSearch", "data", + "ruleRegistry", "share" ], "requiredBundles": ["kibanaReact"], diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx new file mode 100644 index 0000000000000..c60490c8a12b1 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { type KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; +import { render, screen } from '@testing-library/react'; +import { AlertsBadge } from './alerts_badge'; +import * as useKibana from '../../hooks/use_kibana'; +import { HostEntity, ServiceEntity } from '../../../common/entities'; + +describe('AlertsBadge', () => { + jest.spyOn(useKibana, 'useKibana').mockReturnValue({ + services: { + http: { + basePath: { + prepend: (path: string) => path, + }, + }, + }, + } as unknown as KibanaReactContextValue); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('render alerts badge for a host entity', () => { + const entity: HostEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'host', + 'entity.displayName': 'foo', + 'entity.identityFields': 'host.name', + 'host.name': 'foo', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 1, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'host.name: "foo"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1'); + }); + it('render alerts badge for a service entity', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'agent.name': 'node', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': 'service.name', + 'service.name': 'bar', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 5, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'service.name: "bar"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5'); + }); + it('render alerts badge for a service entity with multiple identity fields', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'agent.name': 'node', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'bar', + 'service.environment': 'prod', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 2, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'service.name: "bar" AND service.environment: "prod"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('2'); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx new file mode 100644 index 0000000000000..ba1b992ff62c1 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import rison from '@kbn/rison'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Entity } from '../../../common/entities'; +import { useKibana } from '../../hooks/use_kibana'; +import { parseIdentityFieldValuesToKql } from '../../../common/utils/parse_identity_field_values_to_kql'; + +export function AlertsBadge({ entity }: { entity: Entity }) { + const { + services: { + http: { basePath }, + }, + } = useKibana(); + + const activeAlertsHref = basePath.prepend( + `/app/observability/alerts?_a=${rison.encode({ + kuery: parseIdentityFieldValuesToKql({ entity }), + status: 'active', + })}` + ); + return ( + + + {entity.alertsCount} + + + ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx new file mode 100644 index 0000000000000..96fb8b3736ead --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonIcon, EuiDataGridColumn, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + ENTITY_DISPLAY_NAME, + ENTITY_LAST_SEEN, + ENTITY_TYPE, +} from '@kbn/observability-shared-plugin/common'; + +const alertsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel', { + defaultMessage: 'Alerts', +}); + +const alertsTooltip = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip', { + defaultMessage: 'The count of the active alerts', +}); + +const entityNameLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel', { + defaultMessage: 'Entity name', +}); +const entityNameTooltip = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.entityNameTooltip', + { + defaultMessage: 'Name of the entity (entity.displayName)', + } +); + +const entityTypeLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeLabel', { + defaultMessage: 'Type', +}); +const entityTypeTooltip = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeTooltip', { + defaultMessage: 'Type of entity (entity.type)', +}); + +const entityLastSeenLabel = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenLabel', + { + defaultMessage: 'Last seen', + } +); +const entityLastSeenToolip = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenTooltip', + { + defaultMessage: 'Timestamp of last received data for entity (entity.lastSeenTimestamp)', + } +); + +const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => ( + <> + {title} + + + + +); + +export const getColumns = ({ + showAlertsColumn, +}: { + showAlertsColumn: boolean; +}): EuiDataGridColumn[] => { + return [ + ...(showAlertsColumn + ? [ + { + id: 'alertsCount', + displayAsText: alertsLabel, + isSortable: true, + display: , + initialWidth: 100, + schema: 'numeric', + }, + ] + : []), + { + id: ENTITY_DISPLAY_NAME, + // keep it for accessibility purposes + displayAsText: entityNameLabel, + display: , + isSortable: true, + }, + { + id: ENTITY_TYPE, + // keep it for accessibility purposes + displayAsText: entityTypeLabel, + display: , + isSortable: true, + }, + { + id: ENTITY_LAST_SEEN, + // keep it for accessibility purposes + displayAsText: entityLastSeenLabel, + display: ( + + ), + defaultSortDirection: 'desc', + isSortable: true, + schema: 'datetime', + }, + ]; +}; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index 8bdfa0d46627c..697bc3304753e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -5,103 +5,32 @@ * 2.0. */ import { - EuiButtonIcon, EuiDataGrid, EuiDataGridCellValueElementProps, - EuiDataGridColumn, EuiDataGridSorting, EuiLoadingSpinner, EuiText, - EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; import { last } from 'lodash'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; +import { EntityColumnIds, EntityType } from '../../../common/entities'; import { APIReturnType } from '../../api'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; +import { getColumns } from './grid_columns'; +import { AlertsBadge } from '../alerts_badge/alerts_badge'; import { EntityName } from './entity_name'; -import { EntityType } from '../../../common/entities'; import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; type LatestEntities = InventoryEntitiesAPIReturnType['entities']; -export type EntityColumnIds = - | typeof ENTITY_DISPLAY_NAME - | typeof ENTITY_LAST_SEEN - | typeof ENTITY_TYPE; - -const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => ( - <> - {title} - - - - -); - -const entityNameLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel', { - defaultMessage: 'Entity name', -}); -const entityTypeLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeLabel', { - defaultMessage: 'Type', -}); -const entityLastSeenLabel = i18n.translate( - 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenLabel', - { - defaultMessage: 'Last seen', - } -); - -const columns: EuiDataGridColumn[] = [ - { - id: ENTITY_DISPLAY_NAME, - // keep it for accessibility purposes - displayAsText: entityNameLabel, - display: ( - - ), - isSortable: true, - }, - { - id: ENTITY_TYPE, - // keep it for accessibility purposes - displayAsText: entityTypeLabel, - display: ( - - ), - isSortable: true, - }, - { - id: ENTITY_LAST_SEEN, - // keep it for accessibility purposes - displayAsText: entityLastSeenLabel, - display: ( - - ), - defaultSortDirection: 'desc', - isSortable: true, - schema: 'datetime', - }, -]; - interface Props { loading: boolean; entities: LatestEntities; @@ -125,8 +54,6 @@ export function EntitiesGrid({ onChangeSort, onFilterByType, }: Props) { - const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id)); - const onSort: EuiDataGridSorting['onSort'] = useCallback( (newSortingColumns) => { const lastItem = last(newSortingColumns); @@ -137,6 +64,19 @@ export function EntitiesGrid({ [onChangeSort] ); + const showAlertsColumn = useMemo( + () => entities?.some((entity) => entity?.alertsCount && entity?.alertsCount > 0), + [entities] + ); + + const columnVisibility = useMemo( + () => ({ + visibleColumns: getColumns({ showAlertsColumn }).map(({ id }) => id), + setVisibleColumns: () => {}, + }), + [showAlertsColumn] + ); + const renderCellValue = useCallback( ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { const entity = entities[rowIndex]; @@ -146,6 +86,9 @@ export function EntitiesGrid({ const columnEntityTableId = columnId as EntityColumnIds; switch (columnEntityTableId) { + case 'alertsCount': + return entity?.alertsCount ? : null; + case ENTITY_TYPE: const entityType = entity[columnEntityTableId]; return ( @@ -203,8 +146,8 @@ export function EntitiesGrid({ 'xpack.inventory.entitiesGrid.euiDataGrid.inventoryEntitiesGridLabel', { defaultMessage: 'Inventory entities grid' } )} - columns={columns} - columnVisibility={{ visibleColumns, setVisibleColumns }} + columns={getColumns({ showAlertsColumn })} + columnVisibility={columnVisibility} rowCount={entities.length} renderCellValue={renderCellValue} gridStyle={{ border: 'horizontal', header: 'shade' }} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts index 10ba7fbe4119e..bf72d5d7832cf 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts @@ -15,24 +15,29 @@ export const entitiesMock = [ 'entity.type': 'host', 'entity.displayName': 'Spider-Man', 'entity.id': '0', + alertsCount: 3, }, { 'entity.lastSeenTimestamp': '2024-06-16T21:48:16.259Z', 'entity.type': 'service', 'entity.displayName': 'Iron Man', 'entity.id': '1', + alertsCount: 3, }, + { 'entity.lastSeenTimestamp': '2024-04-28T03:31:57.528Z', 'entity.type': 'host', 'entity.displayName': 'Captain America', 'entity.id': '2', + alertsCount: 10, }, { 'entity.lastSeenTimestamp': '2024-05-14T11:32:04.275Z', 'entity.type': 'host', 'entity.displayName': 'Hulk', 'entity.id': '3', + alertsCount: 1, }, { 'entity.lastSeenTimestamp': '2023-12-05T13:33:54.028Z', @@ -1630,6 +1635,7 @@ export const entitiesMock = [ 'entity.displayName': 'Sed dignissim libero a diam sagittis, in convallis leo pellentesque. Cras ut sapien sed lacus scelerisque vehicula. Pellentesque at purus pulvinar, mollis justo hendrerit, pharetra purus. Morbi dapibus, augue et volutpat ultricies, neque quam sollicitudin mauris, vitae luctus ex libero id erat. Suspendisse risus lectus, scelerisque vel odio sed.', 'entity.id': '269', + alertsCount: 4, }, { 'entity.lastSeenTimestamp': '2023-10-22T13:49:53.092Z', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx index 90b6213da84a4..ee3014e990b0b 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx @@ -17,10 +17,9 @@ import { ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; -import { defaultEntityDefinitions } from '../../../common/entities'; +import { defaultEntityDefinitions, EntityColumnIds } from '../../../common/entities'; import { useInventoryParams } from '../../hooks/use_inventory_params'; import { useKibana } from '../../hooks/use_kibana'; -import { EntityColumnIds } from '../entities_grid'; const ACTIVE_COLUMNS: EntityColumnIds[] = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN]; diff --git a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx index 7af9a9fc21acc..965434eeac6d1 100644 --- a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx @@ -7,7 +7,7 @@ import { EuiDataGridSorting } from '@elastic/eui'; import React from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { EntityType } from '../../../common/entities'; +import { EntityColumnIds, EntityType } from '../../../common/entities'; import { EntitiesGrid } from '../../components/entities_grid'; import { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider'; import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async'; @@ -76,7 +76,7 @@ export function InventoryPage() { path: {}, query: { ...query, - sortField: sorting.id, + sortField: sorting.id as EntityColumnIds, sortDirection: sorting.direction, }, }); diff --git a/x-pack/plugins/observability_solution/inventory/public/plugin.ts b/x-pack/plugins/observability_solution/inventory/public/plugin.ts index 4567e8f34a94a..b6771d2f95550 100644 --- a/x-pack/plugins/observability_solution/inventory/public/plugin.ts +++ b/x-pack/plugins/observability_solution/inventory/public/plugin.ts @@ -117,7 +117,7 @@ export class InventoryPlugin defaultMessage: 'Inventory', }), euiIconType: 'logoObservability', - appRoute: '/app/observability/inventory', + appRoute: '/app/inventory', category: DEFAULT_APP_CATEGORIES.observability, visibleIn: ['sideNav', 'globalSearch'], order: 8200, diff --git a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx index d67a7250f75a5..dc7ba13451e02 100644 --- a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx @@ -8,10 +8,9 @@ import { toNumberRt } from '@kbn/io-ts-utils'; import { Outlet, createRouter } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; -import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common'; import { InventoryPageTemplate } from '../components/inventory_page_template'; import { InventoryPage } from '../pages/inventory_page'; -import { entityTypesRt } from '../../common/entities'; +import { defaultEntitySortField, entityTypesRt, entityColumnIdsRt } from '../../common/entities'; /** * The array of route definitions to be used when the application @@ -27,7 +26,7 @@ const inventoryRoutes = { params: t.type({ query: t.intersection([ t.type({ - sortField: t.string, + sortField: entityColumnIdsRt, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), pageIndex: toNumberRt, }), @@ -39,7 +38,7 @@ const inventoryRoutes = { }), defaults: { query: { - sortField: ENTITY_LAST_SEEN, + sortField: defaultEntitySortField, sortDirection: 'desc', pageIndex: '0', }, diff --git a/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts b/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts new file mode 100644 index 0000000000000..150e946fd98d6 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; +import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import { InventoryRouteHandlerResources } from '../../routes/types'; + +export type AlertsClient = Awaited>; + +export async function createAlertsClient({ + plugins, + request, +}: Pick) { + const ruleRegistryPluginStart = await plugins.ruleRegistry.start(); + const alertsClient = await ruleRegistryPluginStart.getRacClientWithRequest(request); + const alertsIndices = await alertsClient.getAuthorizedAlertsIndices([ + 'logs', + 'infrastructure', + 'apm', + 'slo', + 'observability', + ]); + + if (!alertsIndices || isEmpty(alertsIndices)) { + throw Error('No alert indices exist'); + } + type RequiredParams = ESSearchRequest & { + size: number; + track_total_hits: boolean | number; + }; + + return { + search( + searchParams: TParams + ): Promise> { + return alertsClient.find({ + ...searchParams, + index: alertsIndices.join(','), + }) as Promise; + }, + }; +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts new file mode 100644 index 0000000000000..03027430116e6 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getGroupByTermsAgg } from './get_group_by_terms_agg'; +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +describe('getGroupByTermsAgg', () => { + it('should return an empty object when fields is empty', () => { + const fields: IdentityFieldsPerEntityType = new Map(); + const result = getGroupByTermsAgg(fields); + expect(result).toEqual({}); + }); + + it('should correctly generate aggregation structure for service, host, and container entity types', () => { + const fields: IdentityFieldsPerEntityType = new Map([ + ['service', ['service.name', 'service.environment']], + ['host', ['host.name']], + ['container', ['container.id', 'foo.bar']], + ]); + + const result = getGroupByTermsAgg(fields); + + expect(result).toEqual({ + service: { + composite: { + size: 500, + sources: [ + { 'service.name': { terms: { field: 'service.name' } } }, + { 'service.environment': { terms: { field: 'service.environment' } } }, + ], + }, + }, + host: { + composite: { + size: 500, + sources: [{ 'host.name': { terms: { field: 'host.name' } } }], + }, + }, + container: { + composite: { + size: 500, + sources: [ + { + 'container.id': { + terms: { field: 'container.id' }, + }, + }, + { + 'foo.bar': { terms: { field: 'foo.bar' } }, + }, + ], + }, + }, + }); + }); + it('should override maxSize when provided', () => { + const fields: IdentityFieldsPerEntityType = new Map([['host', ['host.name']]]); + const result = getGroupByTermsAgg(fields, 10); + expect(result.host.composite.size).toBe(10); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts new file mode 100644 index 0000000000000..96ab3eb24444a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +export const getGroupByTermsAgg = (fields: IdentityFieldsPerEntityType, maxSize = 500) => { + return Array.from(fields).reduce((acc, [entityType, identityFields]) => { + acc[entityType] = { + composite: { + size: maxSize, + sources: identityFields.map((field) => ({ + [field]: { + terms: { + field, + }, + }, + })), + }, + }; + return acc; + }, {} as Record); +}; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts new file mode 100644 index 0000000000000..90bf2967b894d --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ContainerEntity, HostEntity, ServiceEntity } from '../../../common/entities'; +import { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '@kbn/observability-shared-plugin/common'; +import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +const commonEntityFields = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + alertCount: 3, +}; +describe('getIdentityFields', () => { + it('should return an empty Map when no entities are provided', () => { + const result = getIdentityFieldsPerEntityType([]); + expect(result.size).toBe(0); + }); + it('should return a Map with unique entity types and their respective identity fields', () => { + const serviceEntity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const hostEntity: HostEntity = { + 'entity.identityFields': ['host.name'], + 'host.name': 'my-host', + 'entity.type': 'host', + 'cloud.provider': null, + ...commonEntityFields, + }; + + const containerEntity: ContainerEntity = { + 'entity.identityFields': 'container.id', + 'host.name': 'my-host', + 'entity.type': 'container', + 'cloud.provider': null, + 'container.id': '123', + ...commonEntityFields, + }; + + const mockEntities = [serviceEntity, hostEntity, containerEntity]; + const result = getIdentityFieldsPerEntityType(mockEntities); + + expect(result.size).toBe(3); + + expect(result.get('service')).toEqual(['service.name', 'service.environment']); + expect(result.get('host')).toEqual(['host.name']); + expect(result.get('container')).toEqual(['container.id']); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts new file mode 100644 index 0000000000000..0ca4eb9d21239 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { Entity, EntityType } from '../../../common/entities'; + +export type IdentityFieldsPerEntityType = Map; + +export const getIdentityFieldsPerEntityType = (entities: Entity[]) => { + const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map(); + + entities.forEach((entity) => + identityFieldsPerEntityType.set(entity[ENTITY_TYPE], [entity[ENTITY_IDENTITY_FIELDS]].flat()) + ); + + return identityFieldsPerEntityType; +}; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts index 853d52d8401a9..e500ce32c3cef 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts @@ -8,11 +8,13 @@ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { kqlQuery } from '@kbn/observability-utils/es/queries/kql_query'; import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, type EntityType, - Entity, + type Entity, + type EntityColumnIds, } from '../../../common/entities'; import { getEntityDefinitionIdWhereClause, getEntityTypesWhereClause } from './query_helper'; @@ -25,15 +27,18 @@ export async function getLatestEntities({ }: { inventoryEsClient: ObservabilityElasticsearchClient; sortDirection: 'asc' | 'desc'; - sortField: string; + sortField: EntityColumnIds; entityTypes?: EntityType[]; kuery?: string; }) { - const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { + // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default. + const entitiesSortField = sortField === 'alertsCount' ? ENTITY_LAST_SEEN : sortField; + + const request = { query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getEntityTypesWhereClause(entityTypes)} | ${getEntityDefinitionIdWhereClause()} - | SORT ${sortField} ${sortDirection} + | SORT ${entitiesSortField} ${sortDirection} | LIMIT ${MAX_NUMBER_OF_ENTITIES} `, filter: { @@ -41,7 +46,9 @@ export async function getLatestEntities({ filter: [...kqlQuery(kuery)], }, }, - }); + }; + + const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', request); return esqlResultToPlainObjects(latestEntitiesEsqlResponse); } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts new file mode 100644 index 0000000000000..4e6ce545a079e --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; +import { AlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; +import { getGroupByTermsAgg } from './get_group_by_terms_agg'; +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; +import { EntityType } from '../../../common/entities'; + +interface Bucket { + key: Record; + doc_count: number; +} + +type EntityTypeBucketsAggregation = Record; + +export async function getLatestEntitiesAlerts({ + alertsClient, + kuery, + identityFieldsPerEntityType, +}: { + alertsClient: AlertsClient; + kuery?: string; + identityFieldsPerEntityType: IdentityFieldsPerEntityType; +}): Promise> { + if (identityFieldsPerEntityType.size === 0) { + return []; + } + + const filter = { + size: 0, + track_total_hits: false, + query: { + bool: { + filter: [...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...kqlQuery(kuery)], + }, + }, + }; + + const response = await alertsClient.search({ + ...filter, + aggs: getGroupByTermsAgg(identityFieldsPerEntityType), + }); + + const aggregations = response.aggregations as EntityTypeBucketsAggregation; + + const alerts = Array.from(identityFieldsPerEntityType).flatMap(([entityType]) => { + const entityAggregation = aggregations?.[entityType]; + + const buckets = entityAggregation.buckets ?? []; + + return buckets.map((bucket: Bucket) => ({ + alertsCount: bucket.doc_count, + type: entityType, + ...bucket.key, + })); + }); + + return alerts; +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index beef1b068ed15..eb80f80d02730 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -8,10 +8,15 @@ import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { jsonRt } from '@kbn/io-ts-utils'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; -import { entityTypeRt } from '../../../common/entities'; +import { orderBy } from 'lodash'; +import { joinByKey } from '@kbn/observability-utils/array/join_by_key'; +import { entityTypeRt, entityColumnIdsRt, Entity } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; +import { createAlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; +import { getLatestEntitiesAlerts } from './get_latest_entities_alerts'; +import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; export const getEntityTypesRoute = createInventoryServerRoute({ endpoint: 'GET /internal/inventory/entities/types', @@ -36,7 +41,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ params: t.type({ query: t.intersection([ t.type({ - sortField: t.string, + sortField: entityColumnIdsRt, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), }), t.partial({ @@ -48,7 +53,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ options: { tags: ['access:inventory'], }, - handler: async ({ params, context, logger }) => { + handler: async ({ params, context, logger, plugins, request }) => { const coreContext = await context.core; const inventoryEsClient = createObservabilityEsClient({ client: coreContext.elasticsearch.client.asCurrentUser, @@ -58,15 +63,40 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ const { sortDirection, sortField, entityTypes, kuery } = params.query; - const latestEntities = await getLatestEntities({ - inventoryEsClient, - sortDirection, - sortField, - entityTypes, + const [alertsClient, latestEntities] = await Promise.all([ + createAlertsClient({ plugins, request }), + getLatestEntities({ + inventoryEsClient, + sortDirection, + sortField, + entityTypes, + kuery, + }), + ]); + + const identityFieldsPerEntityType = getIdentityFieldsPerEntityType(latestEntities); + + const alerts = await getLatestEntitiesAlerts({ + identityFieldsPerEntityType, + alertsClient, kuery, }); - return { entities: latestEntities }; + const joined = joinByKey( + [...latestEntities, ...alerts], + [...identityFieldsPerEntityType.values()].flat() + ).filter((entity) => entity['entity.id']); + + return { + entities: + sortField === 'alertsCount' + ? orderBy( + joined, + [(item: Entity) => item?.alertsCount === undefined, sortField], + ['asc', sortDirection] // push entities without alertsCount to the end + ) + : joined, + }; }, }); diff --git a/x-pack/plugins/observability_solution/inventory/server/types.ts b/x-pack/plugins/observability_solution/inventory/server/types.ts index 05f75561674c6..d3d5ef0fb7f60 100644 --- a/x-pack/plugins/observability_solution/inventory/server/types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/types.ts @@ -14,6 +14,10 @@ import type { DataViewsServerPluginStart, } from '@kbn/data-views-plugin/server'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { + RuleRegistryPluginStartContract, + RuleRegistryPluginSetupContract, +} from '@kbn/rule-registry-plugin/server'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ConfigSchema {} @@ -23,12 +27,14 @@ export interface InventorySetupDependencies { inference: InferenceServerSetup; dataViews: DataViewsServerPluginSetup; features: FeaturesPluginSetup; + ruleRegistry: RuleRegistryPluginSetupContract; } export interface InventoryStartDependencies { entityManager: EntityManagerServerPluginStart; inference: InferenceServerStart; dataViews: DataViewsServerPluginStart; + ruleRegistry: RuleRegistryPluginStartContract; } export interface InventoryServerSetup {} diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 6492cd51d067a..c4c8f5d3ac59d 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -46,6 +46,10 @@ "@kbn/elastic-agent-utils", "@kbn/custom-icons", "@kbn/ui-theme", + "@kbn/rison", + "@kbn/rule-registry-plugin", + "@kbn/observability-plugin", + "@kbn/rule-data-utils", "@kbn/spaces-plugin", "@kbn/cloud-plugin" ] diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/alerting_callout/alerting_callout.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/alerting_callout/alerting_callout.tsx index 397b1597107c4..a6353c674d7c0 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/alerting_callout/alerting_callout.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/alerting_callout/alerting_callout.tsx @@ -11,7 +11,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui'; import { syntheticsSettingsLocatorID } from '@kbn/observability-plugin/common'; import { useFetcher } from '@kbn/observability-shared-plugin/public'; -import { useSessionStorage } from 'react-use'; +import useSessionStorage from 'react-use/lib/useSessionStorage'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import { useKibana } from '@kbn/kibana-react-plugin/public'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/controlled_field.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/controlled_field.tsx index cc37a530087c4..ddf1db76d819f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/controlled_field.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/controlled_field.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useState } from 'react'; import { EuiFormRow, EuiFormRowProps } from '@elastic/eui'; import { useSelector } from 'react-redux'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { ControllerRenderProps, ControllerFieldState, useFormContext } from 'react-hook-form'; import { useKibanaSpace, useIsEditFlow } from '../hooks'; import { selectServiceLocationsState } from '../../../state'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts index 8eaa80fb44a53..710ff65de7c66 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { useLocation } from 'react-router-dom'; import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts index 29e1f550d43cf..df8be3c98b451 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts @@ -7,7 +7,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { useMonitorFiltersState } from '../common/monitor_filters/use_filters'; import { fetchMonitorListAction, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx index f9f0a417e065e..6fcf90f631fad 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx @@ -19,7 +19,7 @@ import { import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; -import { useKey } from 'react-use'; +import useKey from 'react-use/lib/useKey'; import { FlyoutParamProps } from '../types'; import { OverviewLoader } from '../overview_loader'; import { useFilteredGroupMonitors } from './use_filtered_group_monitors'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx index d72d92156e42e..2ff3ea547ae9f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/global_params/params_list.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { TableTitle } from '../../common/components/table_title'; import { ParamsText } from './params_text'; import { SyntheticsParams } from '../../../../../../common/runtime_types'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx index 9f3902b8ccaf2..68f6910b43b78 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx @@ -15,7 +15,7 @@ import React, { FC, } from 'react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { useEvent } from 'react-use'; +import useEvent from 'react-use/lib/useEvent'; import moment from 'moment'; import { Subject } from 'rxjs'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx index 6a07150070362..5e524eca31bda 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx @@ -9,7 +9,7 @@ import { ChromeBreadcrumb } from '@kbn/core/public'; import { render } from '../utils/testing'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Route } from 'react-router-dom'; +import { Route } from '@kbn/shared-ux-router'; import { OVERVIEW_ROUTE } from '../../../../common/constants'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitor_name.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitor_name.ts index 717399d94d1fc..b90044725d070 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitor_name.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_monitor_name.ts @@ -7,7 +7,7 @@ import { useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { useDebounce } from 'react-use'; +import useDebounce from 'react-use/lib/useDebounce'; import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { fetchMonitorManagementList, getMonitorListPageStateWithDefaults } from '../state'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/formatting/test_helpers.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/formatting/test_helpers.ts index 0b32c4a2420e8..8ba26624f3f0c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/formatting/test_helpers.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/formatting/test_helpers.ts @@ -8,6 +8,7 @@ import moment from 'moment'; import { Moment } from 'moment-timezone'; import * as redux from 'react-redux'; +// eslint-disable-next-line no-restricted-imports import * as reactRouterDom from 'react-router-dom'; export function mockMoment() { diff --git a/x-pack/plugins/search_indices/public/code_examples/javascript.ts b/x-pack/plugins/search_indices/public/code_examples/javascript.ts index 3e91cb99301a7..a819b973388f4 100644 --- a/x-pack/plugins/search_indices/public/code_examples/javascript.ts +++ b/x-pack/plugins/search_indices/public/code_examples/javascript.ts @@ -19,7 +19,7 @@ export const JAVASCRIPT_INFO: CodeLanguage = { codeBlockLanguage: 'javascript', }; -const SERVERLESS_INSTALL_CMD = `npm install @elastic/elasticsearch-serverless`; +const SERVERLESS_INSTALL_CMD = `npm install @elastic/elasticsearch`; export const JavascriptServerlessCreateIndexExamples: CreateIndexLanguageExamples = { default: { @@ -28,7 +28,7 @@ export const JavascriptServerlessCreateIndexExamples: CreateIndexLanguageExample elasticsearchURL, apiKey, indexName, - }) => `import { Client } from "@elastic/elasticsearch-serverless" + }) => `import { Client } from "@elastic/elasticsearch" const client = new Client({ node: '${elasticsearchURL}', @@ -47,7 +47,7 @@ client.indices.create({ elasticsearchURL, apiKey, indexName, - }) => `import { Client } from "@elastic/elasticsearch-serverless" + }) => `import { Client } from "@elastic/elasticsearch" const client = new Client({ node: '${elasticsearchURL}', diff --git a/x-pack/plugins/search_indices/public/code_examples/python.ts b/x-pack/plugins/search_indices/public/code_examples/python.ts index e41e542456e72..ac405cfecd1e9 100644 --- a/x-pack/plugins/search_indices/public/code_examples/python.ts +++ b/x-pack/plugins/search_indices/public/code_examples/python.ts @@ -23,7 +23,7 @@ export const PYTHON_INFO: CodeLanguage = { codeBlockLanguage: 'python', }; -const SERVERLESS_PYTHON_INSTALL_CMD = 'pip install elasticsearch-serverless'; +const SERVERLESS_PYTHON_INSTALL_CMD = 'pip install elasticsearch'; export const PythonServerlessCreateIndexExamples: CreateIndexLanguageExamples = { default: { @@ -32,7 +32,7 @@ export const PythonServerlessCreateIndexExamples: CreateIndexLanguageExamples = elasticsearchURL, apiKey, indexName, - }: CodeSnippetParameters) => `from elasticsearch-serverless import Elasticsearch + }: CodeSnippetParameters) => `from elasticsearch import Elasticsearch client = Elasticsearch( "${elasticsearchURL}", @@ -49,21 +49,21 @@ client.indices.create( elasticsearchURL, apiKey, indexName, - }: CodeSnippetParameters) => `from elasticsearch-serverless import Elasticsearch + }: CodeSnippetParameters) => `from elasticsearch import Elasticsearch client = Elasticsearch( - "${elasticsearchURL}", - api_key="${apiKey ?? API_KEY_PLACEHOLDER}" + "${elasticsearchURL}", + api_key="${apiKey ?? API_KEY_PLACEHOLDER}" ) client.indices.create( - index="${indexName ?? INDEX_PLACEHOLDER}" - mappings={ - "properties": { - "vector": {"type": "dense_vector", "dims": 3 }, - "text": {"type": "text"} - } - } + index="${indexName ?? INDEX_PLACEHOLDER}", + mappings={ + "properties": { + "vector": {"type": "dense_vector", "dims": 3 }, + "text": {"type": "text"} + } + } )`, }, }; @@ -72,7 +72,7 @@ const serverlessIngestionCommand: IngestCodeSnippetFunction = ({ apiKey, indexName, sampleDocument, -}) => `from elasticsearch-serverless import Elasticsearch, helpers +}) => `from elasticsearch import Elasticsearch, helpers client = Elasticsearch( "${elasticsearchURL}", @@ -93,25 +93,20 @@ const serverlessUpdateMappingsCommand: IngestCodeSnippetFunction = ({ apiKey, indexName, mappingProperties, -}) => `from elasticsearch-serverless import Elasticsearch +}) => `from elasticsearch import Elasticsearch client = Elasticsearch( -"${elasticsearchURL}", -api_key="${apiKey ?? API_KEY_PLACEHOLDER}" + "${elasticsearchURL}", + api_key="${apiKey ?? API_KEY_PLACEHOLDER}" ) index_name = "${indexName}" mappings = ${JSON.stringify({ properties: mappingProperties }, null, 4)} -update_mapping_response = client.indices.put_mapping(index=index_name, body=mappings) - -# Print the response -print(update_mapping_response) - -# Verify the mapping -mapping = client.indices.get_mapping(index=index_name) -print(mapping)`; +mapping_response = client.indices.put_mapping(index=index_name, body=mappings) +print(mapping_response) +`; export const PythonServerlessVectorsIngestDataExample: IngestDataCodeDefinition = { installCommand: SERVERLESS_PYTHON_INSTALL_CMD, diff --git a/x-pack/plugins/search_indices/public/components/shared/code_sample.tsx b/x-pack/plugins/search_indices/public/components/shared/code_sample.tsx index 4ddce94d685b0..fc233e498ea10 100644 --- a/x-pack/plugins/search_indices/public/components/shared/code_sample.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/code_sample.tsx @@ -54,6 +54,11 @@ export const CodeSample = ({ id, title, language, code, onCodeCopyClick }: CodeS paddingSize="m" isCopyable transparentBackground + css={{ + '*::selection': { + backgroundColor: 'rgba(255, 255, 255, 0.2)', + }, + }} > {code} diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts index 558a9b8371068..b14ddc1e8af9e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -8,7 +8,8 @@ /* eslint-disable max-classes-per-file */ import type { DeepPartial } from 'utility-types'; -import { merge, set } from 'lodash'; +import { merge } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { gte } from 'semver'; import type { EndpointCapabilities } from '../service/response_actions/constants'; import { BaseDataGenerator } from './base_data_generator'; diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts index 5d7cc61d1d7bd..603ec6b1ac6e3 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts @@ -17,7 +17,8 @@ import { checkIfPopupMessagesContainCustomNotifications, resetCustomNotifications, } from './policy_config_helpers'; -import { get, merge, set } from 'lodash'; +import { get, merge } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; describe('Policy Config helpers', () => { describe('disableProtections', () => { diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts index 9b3906191b698..5079493724d78 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { get, set } from 'lodash'; +import { get } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { DefaultPolicyNotificationMessage } from './policy_config'; import type { PolicyConfig } from '../types'; import { PolicyOperatingSystem, ProtectionModes, AntivirusRegistrationModes } from '../types'; diff --git a/x-pack/plugins/security_solution/common/utils/expand_dotted.ts b/x-pack/plugins/security_solution/common/utils/expand_dotted.ts index e919b71dcdcf4..d452ca4df9fb6 100644 --- a/x-pack/plugins/security_solution/common/utils/expand_dotted.ts +++ b/x-pack/plugins/security_solution/common/utils/expand_dotted.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { merge, setWith } from 'lodash'; +import { merge } from 'lodash'; +import { setWith } from '@kbn/safer-lodash-set'; /* * Expands an object with "dotted" fields to a nested object with unflattened fields. diff --git a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts index dfdc2a5ede83f..3d105c34515b4 100644 --- a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts @@ -12,7 +12,7 @@ import { createAddToTimelineCellActionFactory } from './add_to_timeline'; import type { CellActionExecutionContext } from '@kbn/cell-actions'; import { GEO_FIELD_TYPE } from '../../../../timelines/components/timeline/body/renderers/constants'; import { createStartServicesMock } from '../../../../common/lib/kibana/kibana_react.mock'; -import { set } from 'lodash/fp'; +import { set } from '@kbn/safer-lodash-set/fp'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; const services = createStartServicesMock(); diff --git a/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.test.tsx b/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.test.tsx index 80b22c42b544e..ed65f8a12a02a 100644 --- a/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.test.tsx +++ b/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.test.tsx @@ -29,10 +29,7 @@ const data = { const mockUseObservable = jest.fn(); -jest.mock('react-use', () => ({ - ...jest.requireActual('react-use'), - useObservable: () => mockUseObservable(), -})); +jest.mock('react-use/lib/useObservable', () => () => mockUseObservable()); jest.mock('../../../common/lib/kibana', () => { const original = jest.requireActual('../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.tsx b/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.tsx index ad88362e9e861..f03be50f39660 100644 --- a/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.tsx +++ b/x-pack/plugins/security_solution/public/app/components/top_values_popover/top_values_popover.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { EuiWrappingPopover } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { StatefulTopN } from '../../../common/components/top_n'; import { getScopeFromPath } from '../../../sourcerer/containers/sourcerer_paths'; import { useSourcererDataView } from '../../../sourcerer/containers'; diff --git a/x-pack/plugins/security_solution/public/assistant/provider.tsx b/x-pack/plugins/security_solution/public/assistant/provider.tsx index 54d4e47edb684..93c65bb463584 100644 --- a/x-pack/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/plugins/security_solution/public/assistant/provider.tsx @@ -23,7 +23,7 @@ import { once } from 'lodash/fp'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { Message } from '@kbn/elastic-assistant-common'; import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { APP_ID } from '../../common'; import { useBasePath, useKibana } from '../common/lib/kibana'; import { useAssistantTelemetry } from './use_assistant_telemetry'; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx index 97f98b81dc153..8a53cd81db96a 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.test.tsx @@ -13,7 +13,7 @@ import { UpsellingService } from '@kbn/security-solution-upselling/service'; import { Router } from '@kbn/shared-ux-router'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { useLocalStorage } from 'react-use'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { TestProviders } from '../../common/mock'; import { ATTACK_DISCOVERY_PATH } from '../../../common/constants'; @@ -38,15 +38,10 @@ const mockConnectors: unknown[] = [ }, ]; -jest.mock('react-use', () => { - const actual = jest.requireActual('react-use'); - - return { - ...actual, - useLocalStorage: jest.fn().mockReturnValue(['test-id', jest.fn()]), - useSessionStorage: jest.fn().mockReturnValue([undefined, jest.fn()]), - }; -}); +jest.mock('react-use/lib/useLocalStorage', () => jest.fn().mockReturnValue(['test-id', jest.fn()])); +jest.mock('react-use/lib/useSessionStorage', () => + jest.fn().mockReturnValue([undefined, jest.fn()]) +); jest.mock( '@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields', diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx index f3981696b3e80..ea5c16fc3cbba 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx @@ -16,7 +16,7 @@ import { import type { AttackDiscoveries, Replacements } from '@kbn/elastic-assistant-common'; import { uniq } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useLocalStorage } from 'react-use'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { SecurityPageName } from '../../../common/constants'; import { HeaderPage } from '../../common/components/header_page'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx index 30ebf658f0020..c436b7ed9feb5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx @@ -14,7 +14,7 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { useKibana } from '../../../lib/kibana'; import { useBreadcrumbsNav } from '../breadcrumbs'; import { SecuritySideNav } from '../security_side_nav'; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx index b1ec30833b396..bcdb9d163164c 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.tsx @@ -10,7 +10,7 @@ import { buildContextMenuForActions } from '@kbn/ui-actions-plugin/public'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { useAsync } from 'react-use'; +import useAsync from 'react-use/lib/useAsync'; import { InputsModelId } from '../../store/inputs/constants'; import { ModalInspectQuery } from '../inspect/modal'; diff --git a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts index 1459c690068b4..c87129319597c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/endpoint/utils/get_host_platform.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { getHostPlatform } from './get_host_platform'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; diff --git a/x-pack/plugins/security_solution/public/common/mock/router.tsx b/x-pack/plugins/security_solution/public/common/mock/router.tsx index d9cf89a74db08..b946c3bd9bd5f 100644 --- a/x-pack/plugins/security_solution/public/common/mock/router.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/router.tsx @@ -5,6 +5,7 @@ * 2.0. */ +// eslint-disable-next-line no-restricted-imports import { Router } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/module_migration import routeData from 'react-router'; diff --git a/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.test.tsx index 69f1b5fcbf4e0..6da409bcf92d9 100644 --- a/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/global_query_string/helpers.test.tsx @@ -16,6 +16,7 @@ import { } from './helpers'; import { renderHook } from '@testing-library/react-hooks'; import { createMemoryHistory } from 'history'; +// eslint-disable-next-line no-restricted-imports import { Router } from 'react-router-dom'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations_help_info.tsx index 08c4a8e22edfd..1b5d3784364b6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations_help_info.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations_help_info.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useToggle } from 'react-use'; +import useToggle from 'react-use/lib/useToggle'; import { EuiLink, EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '../../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_help_info.tsx index 187f05880d205..9cc1a085507a7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_help_info.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_help_info.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useToggle } from 'react-use'; +import useToggle from 'react-use/lib/useToggle'; import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import * as defineRuleI18n from '../../../rule_creation_ui/components/step_define_rule/translations'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx index a2b7e1a360150..e1eaa9b1e96cd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useToggle } from 'react-use'; +import useToggle from 'react-use/lib/useToggle'; import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/kql_query.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/kql_query.tsx index abd3c93550694..69a00436b6992 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/kql_query.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/kql_query.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback } from 'react'; -import { useToggle } from 'react-use'; +import useToggle from 'react-use/lib/useToggle'; import { css } from '@emotion/css'; import { EuiButtonEmpty } from '@elastic/eui'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info.tsx index 766692e9efecd..51e0c5097b97d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side_help_info.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useToggle } from 'react-use'; +import useToggle from 'react-use/lib/useToggle'; import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx index b4ff6ab29a3ff..6fbdd5b4f8910 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx @@ -16,7 +16,7 @@ import { EuiPopover, } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { useBoolean } from 'react-use'; +import useBoolean from 'react-use/lib/useBoolean'; import { useUserData } from '../../../../../detections/components/user_info'; import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx index ea83efae768fa..6ea9e9dd6a749 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx @@ -16,7 +16,7 @@ import { EuiPopover, } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { useBoolean } from 'react-use'; +import useBoolean from 'react-use/lib/useBoolean'; import type { Rule } from '../../../../rule_management/logic'; import type { RuleSignatureId } from '../../../../../../common/api/detection_engine'; import type { AddPrebuiltRulesTableActions } from './add_prebuilt_rules_table_context'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx index e29dca9d48f3d..51ebecedac3d4 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx @@ -34,7 +34,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; -import { useToggle } from 'react-use'; +import useToggle from 'react-use/lib/useToggle'; import { PICK_ASSET_CRITICALITY } from './translations'; import { AssetCriticalityBadge } from './asset_criticality_badge'; import type { Entity, State } from './use_asset_criticality'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx b/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx index 048b37915e0f4..835265c7402fe 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/routes.tsx @@ -5,8 +5,7 @@ * 2.0. */ import React from 'react'; -import { Switch } from 'react-router-dom'; -import { Route } from '@kbn/shared-ux-router'; +import { Route, Routes } from '@kbn/shared-ux-router'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; @@ -33,14 +32,14 @@ const EntityAnalyticsManagementTelemetry = () => ( const EntityAnalyticsManagementContainer: React.FC = React.memo(() => { return ( - + - + ); }); EntityAnalyticsManagementContainer.displayName = 'EntityAnalyticsManagementContainer'; @@ -56,14 +55,14 @@ const EntityAnalyticsAssetClassificationTelemetry = () => ( const EntityAnalyticsAssetClassificationContainer: React.FC = React.memo(() => { return ( - + - + ); }); diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx index 57cbbb4bc65e6..19b3f653b8f14 100644 --- a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { Router, useParams } from 'react-router-dom'; +import { Router } from '@kbn/shared-ux-router'; +import { useParams } from 'react-router-dom'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { TestProviders } from '../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/management/components/privileged_route/privileged_route.test.tsx b/x-pack/plugins/security_solution/public/management/components/privileged_route/privileged_route.test.tsx index 2fdac844b5e41..32294d09ea82d 100644 --- a/x-pack/plugins/security_solution/public/management/components/privileged_route/privileged_route.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/privileged_route/privileged_route.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +// eslint-disable-next-line no-restricted-imports import { Switch, MemoryRouter } from 'react-router-dom'; import type { AppContextTestRender } from '../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.test.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.test.ts index 85647202a755c..6feaeb878790c 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.test.ts @@ -16,7 +16,7 @@ import { DefaultPolicyNotificationMessage, DefaultPolicyRuleNotificationMessage, } from '../../../../common/endpoint/models/policy_config'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { API_VERSIONS } from '@kbn/fleet-plugin/common'; const useQueryMock = _useQuery as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx index 937804565e29f..b86c79c46242d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.test.tsx @@ -18,7 +18,7 @@ import { AdvancedSection } from './advanced_section'; import userEvent from '@testing-library/user-event'; import { AdvancedPolicySchema } from '../../../models/advanced_policy_schema'; import { within } from '@testing-library/react'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; jest.mock('../../../../../../common/hooks/use_license'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.test.tsx index c55f0793027e5..35cf98b5f5075 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.test.tsx @@ -17,7 +17,8 @@ import { SWITCH_LABEL, } from './attack_surface_reduction_card'; import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import userEvent from '@testing-library/user-event'; import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks'; import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.test.tsx index 9a5c9db321b4b..94399b7c33c4c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.test.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license'; import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license'; import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { ProtectionModes } from '../../../../../../../../common/endpoint/types'; import type { BehaviourProtectionCardProps } from './protection_seetings_card/behaviour_protection_card'; import { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.test.tsx index 7be10cb5ca6d0..f28b379ce5140 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.test.tsx @@ -12,7 +12,7 @@ import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endp import React from 'react'; import type { LinuxEventCollectionCardProps } from './linux_event_collection_card'; import { LinuxEventCollectionCard } from './linux_event_collection_card'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; describe('Policy Linux Event Collection Card', () => { const testSubj = getPolicySettingsFormTestSubjects('test').linuxEvents; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.test.tsx index ac2c81da1c121..d951975d467f0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.test.tsx @@ -10,7 +10,7 @@ import type { AppContextTestRender } from '../../../../../../../common/mock/endp import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint'; import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import React from 'react'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { MacEventCollectionCardProps } from './mac_event_collection_card'; import { MacEventCollectionCard } from './mac_event_collection_card'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx index c4060cf0d7de0..d4ca438c5e25f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx @@ -19,7 +19,8 @@ import type { MalwareProtectionsProps } from './malware_protections_card'; import { MalwareProtectionsCard } from './malware_protections_card'; import type { PolicyConfig } from '../../../../../../../../common/endpoint/types'; import { ProtectionModes } from '../../../../../../../../common/endpoint/types'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import userEvent from '@testing-library/user-event'; jest.mock('../../../../../../../common/hooks/use_license'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.test.tsx index 35ee4eb4fd2d5..4d5236a559985 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.test.tsx @@ -11,7 +11,7 @@ import { createAppRootMockRenderer } from '../../../../../../../common/mock/endp import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import React from 'react'; import { ProtectionModes } from '../../../../../../../../common/endpoint/types'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { MemoryProtectionCardProps } from './memory_protection_card'; import { LOCKED_CARD_MEMORY_TITLE, MemoryProtectionCard } from './memory_protection_card'; import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.test.tsx index 1970c5915fe07..d78840a44e9ce 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.test.tsx @@ -11,7 +11,7 @@ import { createAppRootMockRenderer } from '../../../../../../../common/mock/endp import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import React from 'react'; import { ProtectionModes } from '../../../../../../../../common/endpoint/types'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks'; import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license'; import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx index 2ee20f4a51a51..4dfe847297ef2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.test.tsx @@ -10,7 +10,7 @@ import type { AppContextTestRender } from '../../../../../../../common/mock/endp import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint'; import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; import React from 'react'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { WindowsEventCollectionCardProps } from './windows_event_collection_card'; import { WindowsEventCollectionCard } from './windows_event_collection_card'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.test.tsx index ee31a690c27db..6616ac02e3c5a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.test.tsx @@ -12,7 +12,8 @@ import React from 'react'; import type { DetectPreventProtectionLevelProps } from './detect_prevent_protection_level'; import { DetectPreventProtectionLevel } from './detect_prevent_protection_level'; import userEvent from '@testing-library/user-event'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { ProtectionModes } from '../../../../../../../common/endpoint/types'; import { expectIsViewOnly, exactMatchText } from '../mocks'; import { createLicenseServiceMock } from '../../../../../../../common/license/mocks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.test.tsx index 51e2fb275a78a..ba2cd95989cde 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.test.tsx @@ -17,7 +17,8 @@ import { EventCollectionCard } from './event_collection_card'; import { OperatingSystem } from '@kbn/securitysolution-utils'; import { expectIsViewOnly, exactMatchText } from '../mocks'; import userEvent from '@testing-library/user-event'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { within } from '@testing-library/react'; describe('Policy Event Collection Card common component', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.tsx index a1fee2d77b01d..8ab5f92da27c0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_collection_card.tsx @@ -19,7 +19,8 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import { cloneDeep, get, set } from 'lodash'; +import { cloneDeep, get } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { EuiCheckboxProps } from '@elastic/eui'; import { getEmptyValue } from '../../../../../../common/components/empty_value'; import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx index b75084f6b97a5..eb0686d7e07ef 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx @@ -20,7 +20,8 @@ import { NotifyUserOption, } from './notify_user_option'; import { expectIsViewOnly, exactMatchText } from '../mocks'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { ProtectionModes } from '../../../../../../../common/endpoint/types'; import userEvent from '@testing-library/user-event'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.test.tsx index 9a2bb55d85ea5..aa57720d58160 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.test.tsx @@ -16,7 +16,8 @@ import type { ProtectionSettingCardSwitchProps } from './protection_setting_card import { ProtectionSettingCardSwitch } from './protection_setting_card_switch'; import { exactMatchText, expectIsViewOnly, setMalwareMode } from '../mocks'; import { ProtectionModes } from '../../../../../../../common/endpoint/types'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import userEvent from '@testing-library/user-event'; jest.mock('../../../../../../common/hooks/use_license'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts index 9448b93c7627e..e51382a6b91a8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { PolicyConfig } from '../../../../../../common/endpoint/types'; import { AntivirusRegistrationModes, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx index 84642ffdc1582..f26d520406407 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx @@ -21,7 +21,8 @@ import { getPolicySettingsFormTestSubjects, setMalwareMode, } from '../policy_settings_form/mocks'; -import { cloneDeep, set } from 'lodash'; +import { cloneDeep } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { ProtectionModes } from '../../../../../../common/endpoint/types'; import { waitFor, cleanup } from '@testing-library/react'; import { packagePolicyRouteService, API_VERSIONS } from '@kbn/fleet-plugin/common'; diff --git a/x-pack/plugins/security_solution/public/notes/routes.tsx b/x-pack/plugins/security_solution/public/notes/routes.tsx index 7bd17c2b012ef..c49f54f9c9a93 100644 --- a/x-pack/plugins/security_solution/public/notes/routes.tsx +++ b/x-pack/plugins/security_solution/public/notes/routes.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { Switch } from 'react-router-dom'; -import { Route } from '@kbn/shared-ux-router'; +import { Route, Routes } from '@kbn/shared-ux-router'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { NoteManagementPage } from './pages/note_management_page'; import { SpyRoute } from '../common/utils/route/spy_routes'; @@ -26,10 +25,10 @@ const NotesManagementTelemetry = () => ( const NotesManagementContainer: React.FC = React.memo(() => { return ( - + - + ); }); NotesManagementContainer.displayName = 'NotesManagementContainer'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx index 27c92d0f0b11f..3a6b5ae3be92c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { useOnboardingService } from '../../../../../hooks/use_onboarding_service'; import { AgentlessAvailableCallout } from './agentless_available_callout'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts index 19e80e4005a59..775ff09546fe6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts @@ -7,7 +7,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useBodyConfig } from './use_body_config'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { hasCapabilities } from '../../../../common/lib/capabilities'; const bodyConfig = [ @@ -43,7 +43,7 @@ const bodyConfig = [ ]; // Mock dependencies -jest.mock('react-use'); +jest.mock('react-use/lib/useObservable'); jest.mock('../../../../common/lib/kibana/kibana_react'); jest.mock('../../../../common/lib/capabilities'); jest.mock('../body_config', () => ({ bodyConfig })); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts index e140f953fb028..f7b12e5988c0d 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { useMemo } from 'react'; import { hasCapabilities } from '../../../../common/lib/capabilities'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx index da316e0d0d907..a79b288dd8562 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; import { LinkCard } from '../common/link_card'; import teammatesImage from './images/teammates_card.png'; diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts index c22c8f0f5310c..eac269f3a4a35 100644 --- a/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts +++ b/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useLocalStorage } from 'react-use'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import type { OnboardingCardId } from '../constants'; import type { IntegrationTabId } from '../components/onboarding_body/cards/integrations/types'; diff --git a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.ts b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.ts index 58e8aced4470b..a0d0ab4dd1061 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { get, has, set, omit, isObject, toString as fpToString } from 'lodash/fp'; +import { get, has, omit, isObject, toString as fpToString } from 'lodash/fp'; +import { set } from '@kbn/safer-lodash-set/fp'; import type { Action, Middleware } from 'redux'; import type { CoreStart } from '@kbn/core/public'; import type { Filter, MatchAllFilter } from '@kbn/es-query'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts index 66b804e07eb10..b3011005a8b76 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.test.ts @@ -71,7 +71,8 @@ import type { HapiReadableStream, SecuritySolutionRequestHandlerContext } from ' import { createHapiReadableStreamMock } from '../../services/actions/mocks'; import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; -import { omit, set } from 'lodash'; +import { omit } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants'; import { responseActionsClientMock } from '../../services/actions/clients/mocks'; import type { ActionsApiRequestHandlerContext } from '@kbn/actions-plugin/server'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts index 20389d41f3956..dafc0b285f489 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/clients/lib/base_response_actions_client.test.ts @@ -36,7 +36,7 @@ import { ENDPOINT_ACTIONS_INDEX, } from '../../../../../../common/endpoint/constants'; import type { DeepMutable } from '../../../../../../common/endpoint/types/utility_types'; -import { set } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import { responseActionsClientMock } from '../mocks'; import type { ResponseActionAgentType } from '../../../../../../common/endpoint/service/response_actions/constants'; import { getResponseActionFeatureKey } from '../../../feature_usage/feature_keys'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/traverse_and_mutate_doc.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/traverse_and_mutate_doc.ts index c9720a139ae7d..128cabf02aca6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/traverse_and_mutate_doc.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/traverse_and_mutate_doc.ts @@ -8,7 +8,8 @@ import { ecsFieldMap } from '@kbn/alerts-as-data-utils'; import { flattenWithPrefix } from '@kbn/securitysolution-rules'; -import { isPlainObject, isArray, set } from 'lodash'; +import { isPlainObject, isArray } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { SearchTypes } from '../../../../../../common/detection_engine/types'; import { isValidIpType } from './ecs_types_validators/is_valid_ip_type'; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index cb33d608ea0c9..0f29a415dfeba 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -8,7 +8,8 @@ import moment from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { PackagePolicy } from '@kbn/fleet-plugin/common/types/models/package_policy'; -import { merge, set } from 'lodash'; +import { merge } from 'lodash'; +import { set } from '@kbn/safer-lodash-set'; import type { Logger, LogMeta } from '@kbn/core/server'; import { sha256 } from 'js-sha256'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; diff --git a/x-pack/plugins/spaces/common/constants.ts b/x-pack/plugins/spaces/common/constants.ts index 14932a93a06b7..232892ab7b9ad 100644 --- a/x-pack/plugins/spaces/common/constants.ts +++ b/x-pack/plugins/spaces/common/constants.ts @@ -52,3 +52,8 @@ export const API_VERSIONS = { v1: '2023-10-31', }, }; + +/** + * The setting to control whether the Space Solution Tour is shown. + */ +export const SHOW_SPACE_SOLUTION_TOUR_SETTING = 'showSpaceSolutionTour'; diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_description.tsx b/x-pack/plugins/spaces/public/nav_control/components/spaces_description.tsx index 982e11ffbf4e7..03667f48f4166 100644 --- a/x-pack/plugins/spaces/public/nav_control/components/spaces_description.tsx +++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_description.tsx @@ -20,7 +20,7 @@ import { getSpacesFeatureDescription } from '../../constants'; interface Props { id: string; isLoading: boolean; - toggleSpaceSelector: () => void; + onClickManageSpaceBtn: () => void; capabilities: Capabilities; navigateToApp: ApplicationStart['navigateToApp']; } @@ -45,7 +45,7 @@ export const SpacesDescription: FC = (props: Props) => { diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx index 47f7d840b9bee..29d360fe91f3f 100644 --- a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx @@ -43,6 +43,7 @@ interface Props { spaces: Space[]; serverBasePath: string; toggleSpaceSelector: () => void; + onClickManageSpaceBtn: () => void; intl: InjectedIntl; capabilities: Capabilities; navigateToApp: ApplicationStart['navigateToApp']; @@ -218,7 +219,7 @@ class SpacesMenuUI extends Component { key="manageSpacesButton" className="spcMenu__manageButton" size="s" - onClick={this.props.toggleSpaceSelector} + onClick={this.props.onClickManageSpaceBtn} capabilities={this.props.capabilities} navigateToApp={this.props.navigateToApp} /> diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control.tsx index 7cb32fff01e1e..1dc888333fdf5 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control.tsx @@ -12,6 +12,7 @@ import ReactDOM from 'react-dom'; import type { CoreStart } from '@kbn/core/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { initTour } from './solution_view_tour'; import type { EventTracker } from '../analytics'; import type { ConfigType } from '../config'; import type { SpacesManager } from '../spaces_manager'; @@ -22,6 +23,8 @@ export function initSpacesNavControl( config: ConfigType, eventTracker: EventTracker ) { + const { showTour$, onFinishTour } = initTour(core, spacesManager); + core.chrome.navControls.registerLeft({ order: 1000, mount(targetDomElement: HTMLElement) { @@ -47,6 +50,8 @@ export function initSpacesNavControl( navigateToUrl={core.application.navigateToUrl} allowSolutionVisibility={config.allowSolutionVisibility} eventTracker={eventTracker} + showTour$={showTour$} + onFinishTour={onFinishTour} /> , diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx index 9b573615c65b9..f1ba5c9f3f3cf 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx @@ -8,7 +8,6 @@ import { EuiFieldSearch, EuiHeaderSectionItemButton, - EuiPopover, EuiSelectable, EuiSelectableListItem, } from '@elastic/eui'; @@ -18,7 +17,7 @@ import * as Rx from 'rxjs'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; -import { NavControlPopover } from './nav_control_popover'; +import { NavControlPopover, type Props as NavControlPopoverProps } from './nav_control_popover'; import type { Space } from '../../common'; import { EventTracker } from '../analytics'; import { SpaceAvatarInternal } from '../space_avatar/space_avatar_internal'; @@ -49,7 +48,12 @@ const reportEvent = jest.fn(); const eventTracker = new EventTracker({ reportEvent }); describe('NavControlPopover', () => { - async function setup(spaces: Space[], allowSolutionVisibility = false, activeSpace?: Space) { + async function setup( + spaces: Space[], + allowSolutionVisibility = false, + activeSpace?: Space, + props?: Partial + ) { const spacesManager = spacesManagerMock.create(); spacesManager.getSpaces = jest.fn().mockResolvedValue(spaces); @@ -68,6 +72,9 @@ describe('NavControlPopover', () => { navigateToUrl={jest.fn()} allowSolutionVisibility={allowSolutionVisibility} eventTracker={eventTracker} + showTour$={Rx.of(false)} + onFinishTour={jest.fn()} + {...props} /> ); @@ -81,7 +88,7 @@ describe('NavControlPopover', () => { it('renders without crashing', () => { const spacesManager = spacesManagerMock.create(); - const { baseElement } = render( + const { baseElement, queryByTestId } = render( { navigateToUrl={jest.fn()} allowSolutionVisibility={false} eventTracker={eventTracker} + showTour$={Rx.of(false)} + onFinishTour={jest.fn()} /> ); expect(baseElement).toMatchSnapshot(); + expect(queryByTestId('spaceSolutionTour')).toBeNull(); }); it('renders a SpaceAvatar with the active space', async () => { @@ -117,6 +127,8 @@ describe('NavControlPopover', () => { navigateToUrl={jest.fn()} allowSolutionVisibility={false} eventTracker={eventTracker} + showTour$={Rx.of(false)} + onFinishTour={jest.fn()} /> ); @@ -223,20 +235,29 @@ describe('NavControlPopover', () => { }); it('can close its popover', async () => { + jest.useFakeTimers(); const wrapper = await setup(mockSpaces); + expect(findTestSubject(wrapper, 'spaceMenuPopoverPanel').exists()).toEqual(false); // closed + + // Open the popover await act(async () => { wrapper.find(EuiHeaderSectionItemButton).find('button').simulate('click'); }); wrapper.update(); - expect(wrapper.find(EuiPopover).props().isOpen).toEqual(true); + expect(findTestSubject(wrapper, 'spaceMenuPopoverPanel').exists()).toEqual(true); // open + // Close the popover await act(async () => { - wrapper.find(EuiPopover).props().closePopover(); + wrapper.find(EuiHeaderSectionItemButton).find('button').simulate('click'); + }); + act(() => { + jest.runAllTimers(); }); wrapper.update(); + expect(findTestSubject(wrapper, 'spaceMenuPopoverPanel').exists()).toEqual(false); // closed - expect(wrapper.find(EuiPopover).props().isOpen).toEqual(false); + jest.useRealTimers(); }); it('should render solution for spaces', async () => { @@ -301,4 +322,42 @@ describe('NavControlPopover', () => { space_id_prev: 'space-1', }); }); + + it('should show the solution view tour', async () => { + jest.useFakeTimers(); // the underlying EUI tour component has a timeout that needs to be flushed for the test to pass + + const spaces: Space[] = [ + { + id: 'space-1', + name: 'Space-1', + disabledFeatures: [], + solution: 'es', + }, + ]; + + const activeSpace = spaces[0]; + const showTour$ = new Rx.BehaviorSubject(true); + const onFinishTour = jest.fn().mockImplementation(() => { + showTour$.next(false); + }); + + const wrapper = await setup(spaces, true /** allowSolutionVisibility **/, activeSpace, { + showTour$, + onFinishTour, + }); + + expect(findTestSubject(wrapper, 'spaceSolutionTour').exists()).toBe(true); + + act(() => { + findTestSubject(wrapper, 'closeTourBtn').simulate('click'); + }); + act(() => { + jest.runAllTimers(); + }); + wrapper.update(); + + expect(findTestSubject(wrapper, 'spaceSolutionTour').exists()).toBe(false); + + jest.useRealTimers(); + }); }); diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx index b9830b2063dd5..d84fac2fdced4 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx @@ -13,13 +13,14 @@ import { withEuiTheme, } from '@elastic/eui'; import React, { Component, lazy, Suspense } from 'react'; -import type { Subscription } from 'rxjs'; +import type { Observable, Subscription } from 'rxjs'; import type { ApplicationStart, Capabilities } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { SpacesDescription } from './components/spaces_description'; import { SpacesMenu } from './components/spaces_menu'; +import { SolutionViewTour } from './solution_view_tour'; import type { Space } from '../../common'; import type { EventTracker } from '../analytics'; import { getSpaceAvatarComponent } from '../space_avatar'; @@ -30,7 +31,7 @@ const LazySpaceAvatar = lazy(() => getSpaceAvatarComponent().then((component) => ({ default: component })) ); -interface Props { +export interface Props { spacesManager: SpacesManager; anchorPosition: PopoverAnchorPosition; capabilities: Capabilities; @@ -40,6 +41,8 @@ interface Props { theme: WithEuiThemeProps['theme']; allowSolutionVisibility: boolean; eventTracker: EventTracker; + showTour$: Observable; + onFinishTour: () => void; } interface State { @@ -47,12 +50,14 @@ interface State { loading: boolean; activeSpace: Space | null; spaces: Space[]; + showTour: boolean; } const popoutContentId = 'headerSpacesMenuContent'; class NavControlPopoverUI extends Component { private activeSpace$?: Subscription; + private showTour$Sub?: Subscription; constructor(props: Props) { super(props); @@ -61,6 +66,7 @@ class NavControlPopoverUI extends Component { loading: false, activeSpace: null, spaces: [], + showTour: false, }; } @@ -72,15 +78,23 @@ class NavControlPopoverUI extends Component { }); }, }); + + this.showTour$Sub = this.props.showTour$.subscribe((showTour) => { + this.setState({ showTour }); + }); } public componentWillUnmount() { this.activeSpace$?.unsubscribe(); + this.showTour$Sub?.unsubscribe(); } public render() { const button = this.getActiveSpaceButton(); const { theme } = this.props; + const { activeSpace } = this.state; + + const isTourOpen = Boolean(activeSpace) && this.state.showTour && !this.state.showSpaceSelector; let element: React.ReactNode; if (this.state.loading || this.state.spaces.length < 2) { @@ -88,9 +102,13 @@ class NavControlPopoverUI extends Component { { + // No need to show the tour anymore, the user is taking action + this.props.onFinishTour(); + this.toggleSpaceSelector(); + }} /> ); } else { @@ -106,24 +124,38 @@ class NavControlPopoverUI extends Component { activeSpace={this.state.activeSpace} allowSolutionVisibility={this.props.allowSolutionVisibility} eventTracker={this.props.eventTracker} + onClickManageSpaceBtn={() => { + // No need to show the tour anymore, the user is taking action + this.props.onFinishTour(); + this.toggleSpaceSelector(); + }} /> ); } return ( - - {element} - + + {element} + + ); } @@ -195,6 +227,7 @@ class NavControlPopoverUI extends Component { protected toggleSpaceSelector = () => { const isOpening = !this.state.showSpaceSelector; + if (isOpening) { this.loadSpaces(); } diff --git a/x-pack/plugins/spaces/public/nav_control/solution_view_tour/index.ts b/x-pack/plugins/spaces/public/nav_control/solution_view_tour/index.ts new file mode 100644 index 0000000000000..d85a76c586925 --- /dev/null +++ b/x-pack/plugins/spaces/public/nav_control/solution_view_tour/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { initTour } from './lib'; + +export { SolutionViewTour } from './solution_view_tour'; diff --git a/x-pack/plugins/spaces/public/nav_control/solution_view_tour/lib.ts b/x-pack/plugins/spaces/public/nav_control/solution_view_tour/lib.ts new file mode 100644 index 0000000000000..7936eea09dab6 --- /dev/null +++ b/x-pack/plugins/spaces/public/nav_control/solution_view_tour/lib.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 { BehaviorSubject, defer, from, map, of, shareReplay, switchMap } from 'rxjs'; + +import type { CoreStart } from '@kbn/core/public'; + +import type { Space } from '../../../common'; +import { + DEFAULT_SPACE_ID, + SHOW_SPACE_SOLUTION_TOUR_SETTING, + SOLUTION_VIEW_CLASSIC, +} from '../../../common/constants'; +import type { SpacesManager } from '../../spaces_manager'; + +export function initTour(core: CoreStart, spacesManager: SpacesManager) { + const showTourUiSettingValue = core.settings.globalClient.get(SHOW_SPACE_SOLUTION_TOUR_SETTING); + const showTour$ = new BehaviorSubject(showTourUiSettingValue ?? true); + + const allSpaces$ = defer(() => from(spacesManager.getSpaces())).pipe(shareReplay(1)); + + const hasMultipleSpaces = (spaces: Space[]) => { + return spaces.length > 1; + }; + + const isDefaultSpaceOnClassic = (spaces: Space[]) => { + const defaultSpace = spaces.find((space) => space.id === DEFAULT_SPACE_ID); + + if (!defaultSpace) { + // Don't show the tour if the default space doesn't exist (this should never happen) + return true; + } + + if (!defaultSpace.solution || defaultSpace.solution === SOLUTION_VIEW_CLASSIC) { + return true; + } + }; + + const showTourObservable$ = showTour$.pipe( + switchMap((showTour) => { + if (!showTour) return of(false); + + return allSpaces$.pipe( + map((spaces) => { + if (hasMultipleSpaces(spaces) || isDefaultSpaceOnClassic(spaces)) { + return false; + } + + return true; + }) + ); + }) + ); + + const hideTourInGlobalSettings = () => { + core.settings.globalClient.set(SHOW_SPACE_SOLUTION_TOUR_SETTING, false).catch(() => { + // Silently swallow errors, the user will just see the tour again next time they load the page + }); + }; + + if (showTourUiSettingValue !== false) { + allSpaces$.subscribe((spaces) => { + if (hasMultipleSpaces(spaces) || isDefaultSpaceOnClassic(spaces)) { + // If we have either (1) multiple space or (2) only one space and it's the default space with the classic solution, + // we don't want to show the tour later on. This can happen in the following scenarios: + // - the user deletes all the spaces but one (and that last space has a solution set) + // - the user edits the default space and sets a solution + // So we can immediately hide the tour in the global settings from now on. + hideTourInGlobalSettings(); + } + }); + } + + const onFinishTour = () => { + hideTourInGlobalSettings(); + showTour$.next(false); + }; + + return { showTour$: showTourObservable$, onFinishTour }; +} diff --git a/x-pack/plugins/spaces/public/nav_control/solution_view_tour/solution_view_tour.tsx b/x-pack/plugins/spaces/public/nav_control/solution_view_tour/solution_view_tour.tsx new file mode 100644 index 0000000000000..bf80bf92bdf4e --- /dev/null +++ b/x-pack/plugins/spaces/public/nav_control/solution_view_tour/solution_view_tour.tsx @@ -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 { EuiButtonEmpty, EuiLink, EuiText, EuiTourStep } from '@elastic/eui'; +import React from 'react'; +import type { FC, PropsWithChildren } from 'react'; + +import type { OnBoardingDefaultSolution } from '@kbn/cloud-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { SolutionView } from '../../../common'; +import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants'; + +const tourLearnMoreLink = 'https://ela.st/left-nav'; + +const LearnMoreLink = () => ( + + {i18n.translate('xpack.spaces.navControl.tour.learnMore', { + defaultMessage: 'Learn more', + })} + +); + +const solutionMap: Record = { + es: i18n.translate('xpack.spaces.navControl.tour.esSolution', { + defaultMessage: 'Search', + }), + security: i18n.translate('xpack.spaces.navControl.tour.securitySolution', { + defaultMessage: 'Security', + }), + oblt: i18n.translate('xpack.spaces.navControl.tour.obltSolution', { + defaultMessage: 'Observability', + }), +}; + +interface Props extends PropsWithChildren<{}> { + solution?: SolutionView; + isTourOpen: boolean; + onFinishTour: () => void; +} + +export const SolutionViewTour: FC = ({ children, solution, isTourOpen, onFinishTour }) => { + const solutionLabel = solution && solution !== SOLUTION_VIEW_CLASSIC ? solutionMap[solution] : ''; + if (!solutionLabel) { + return children; + } + + return ( + +

+ , + }} + /> +

+ + } + isStepOpen={isTourOpen} + minWidth={300} + maxWidth={360} + onFinish={onFinishTour} + step={1} + stepsTotal={1} + title={i18n.translate('xpack.spaces.navControl.tour.title', { + defaultMessage: 'You chose the {solution} solution view', + values: { solution: solutionLabel }, + })} + anchorPosition="downCenter" + footerAction={ + + {i18n.translate('xpack.spaces.navControl.tour.closeBtn', { + defaultMessage: 'Close', + })} + + } + panelProps={{ + 'data-test-subj': 'spaceSolutionTour', + }} + > + <>{children} +
+ ); +}; diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 2f8fb2ec30842..e36a6fb3cc7f1 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -35,6 +35,7 @@ import { SpacesClientService } from './spaces_client'; import type { SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; import { SpacesService } from './spaces_service'; import type { SpacesRequestHandlerContext } from './types'; +import { getUiSettings } from './ui_settings'; import { registerSpacesUsageCollector } from './usage_collection'; import { UsageStatsService } from './usage_stats'; import { SpacesLicenseService } from '../common/licensing'; @@ -149,6 +150,7 @@ export class SpacesPlugin public setup(core: CoreSetup, plugins: PluginsSetup): SpacesPluginSetup { this.onCloud$.next(plugins.cloud !== undefined && plugins.cloud.isCloudEnabled); const spacesClientSetup = this.spacesClientService.setup({ config$: this.config$ }); + core.uiSettings.registerGlobal(getUiSettings()); const spacesServiceSetup = this.spacesService.setup({ basePath: core.http.basePath, diff --git a/x-pack/plugins/spaces/server/ui_settings.ts b/x-pack/plugins/spaces/server/ui_settings.ts new file mode 100644 index 0000000000000..cfb6c996296da --- /dev/null +++ b/x-pack/plugins/spaces/server/ui_settings.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 { schema } from '@kbn/config-schema'; +import type { UiSettingsParams } from '@kbn/core/types'; + +import { SHOW_SPACE_SOLUTION_TOUR_SETTING } from '../common/constants'; + +/** + * uiSettings definitions for Spaces + */ +export const getUiSettings = (): Record => { + return { + [SHOW_SPACE_SOLUTION_TOUR_SETTING]: { + schema: schema.boolean(), + readonly: true, + readonlyMode: 'ui', + }, + }; +}; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index af71b7b1b9eda..c9713d7d10c73 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -16858,10 +16858,8 @@ "xpack.enterpriseSearch.content.new_index.genericTitle": "Nouvel index de recherche", "xpack.enterpriseSearch.content.new_index.successToast.title": "L’index a bien été créé", "xpack.enterpriseSearch.content.new_web_crawler.breadcrumbs": "Nouveau robot d'indexation", - "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error": "Un connecteur nommé {connectorName} existe déjà", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.createIndex.buttonText": "Créer un connecteur", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.formTitle": "Créer un connecteur", - "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error": "{connectorName} est un nom de connecteur non valide", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText": "En savoir plus sur les connecteurs", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputHelpText.lineTwo": "Les noms doivent être en minuscules et ne peuvent pas contenir d'espaces ni de caractères spéciaux.", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputLabel": "Nom du connecteur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cdd8afc68af2e..d123d0edd8948 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16604,10 +16604,8 @@ "xpack.enterpriseSearch.content.new_index.genericTitle": "新しい検索インデックス", "xpack.enterpriseSearch.content.new_index.successToast.title": "インデックスが正常に作成されました", "xpack.enterpriseSearch.content.new_web_crawler.breadcrumbs": "新しいWebクローラー", - "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error": "名前\"{connectorName}\"のコネクターはすでに存在しています", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.createIndex.buttonText": "コネクターを作成", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.formTitle": "コネクターを作成する", - "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error": "{connectorName}は無効なコネクター名です", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText": "コネクターの詳細", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputHelpText.lineTwo": "名前は小文字で入力してください。スペースや特殊文字は使用できません。", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputLabel": "コネクター名", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b94fb455c8ad5..3e658947b010b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16633,10 +16633,8 @@ "xpack.enterpriseSearch.content.new_index.genericTitle": "新搜索索引", "xpack.enterpriseSearch.content.new_index.successToast.title": "已成功创建索引", "xpack.enterpriseSearch.content.new_web_crawler.breadcrumbs": "新网络爬虫", - "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error": "名为 {connectorName} 的连接器已存在", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.createIndex.buttonText": "创建连接器", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.formTitle": "创建连接器", - "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error": "{connectorName} 为无效的连接器名称", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText": "详细了解连接器", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputHelpText.lineTwo": "名称应为小写,并且不能包含空格或特殊字符。", "xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputLabel": "连接器名称", diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx b/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx index 183b1acd3ca53..233b673353929 100644 --- a/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx +++ b/x-pack/plugins/triggers_actions_ui/.storybook/decorator.tsx @@ -69,7 +69,7 @@ export const StorybookContextDecorator: FC; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 8a6003960473b..8d39d7851d9bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -25,6 +25,8 @@ export const routeToConnectors = `/connectors`; export const routeToConnectorEdit = `/connectors/:connectorId`; export const routeToRules = `/rules`; export const routeToLogs = `/logs`; +export const routeToCreateRule = '/rules/create'; +export const routeToEditRule = '/rules/edit'; export const legacyRouteToAlerts = `/alerts`; export const legacyRouteToRuleDetails = `/alert/:alertId`; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 045732830c891..a2a2187c75895 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -72,6 +72,7 @@ export const TriggersActionsUIHome: React.FunctionComponent { defaultMessage: 'Rules', }); break; + case 'createRule': + updatedTitle = i18n.translate('xpack.triggersActionsUI.rules.createRule.breadcrumbTitle', { + defaultMessage: 'Create rule', + }); + break; + case 'editRule': + updatedTitle = i18n.translate('xpack.triggersActionsUI.rules.editRule.breadcrumbTitle', { + defaultMessage: 'Edit rule', + }); + break; case 'alerts': updatedTitle = i18n.translate('xpack.triggersActionsUI.alerts.breadcrumbTitle', { defaultMessage: 'Alerts', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/rules_app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/rules_app.tsx index 9f472c251a91b..8550518edb457 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/rules_app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/rules_app.tsx @@ -31,7 +31,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; -import { ruleDetailsRoute } from '@kbn/rule-data-utils'; +import { ruleDetailsRoute, createRuleRoute, editRuleRoute } from '@kbn/rule-data-utils'; import { QueryClientProvider } from '@tanstack/react-query'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; @@ -54,11 +54,14 @@ import { KibanaContextProvider, useKibana } from '../common/lib/kibana'; import { ConnectorProvider } from './context/connector_context'; import { ALERTS_PAGE_ID, CONNECTORS_PLUGIN_ID } from '../common/constants'; import { queryClient } from './query_client'; +import { getIsExperimentalFeatureEnabled } from '../common/get_experimental_features'; const TriggersActionsUIHome = lazy(() => import('./home')); const RuleDetailsRoute = lazy( () => import('./sections/rule_details/components/rule_details_route') ); +const CreateRuleRoute = lazy(() => import('./sections/rule_form/rule_form_route')); +const EditRuleRoute = lazy(() => import('./sections/rule_form/rule_form_route')); export interface TriggersAndActionsUiServices extends CoreStart { actions: ActionsPublicPluginSetup; @@ -122,9 +125,25 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) = application: { navigateToApp }, } = useKibana().services; + const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); + return ( + {!isUsingRuleCreateFlyout && ( + + )} + {!isUsingRuleCreateFlyout && ( + + )} - diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index c6598becec313..ca4de13be903b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -199,6 +199,7 @@ export function RuleComponent({ actionTypeRegistry, ruleTypeRegistry, hideEditButton: true, + useNewRuleForm: true, onEditRule: requestRefresh, })} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx index 91a77d18009c5..bc5da8218d118 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.test.tsx @@ -21,6 +21,10 @@ jest.mock('./rule_actions', () => ({ }, })); +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); + jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), hasSaveRulesCapability: jest.fn(() => true), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx index e608a71af05a6..ed21bb88992a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx @@ -16,13 +16,14 @@ import { EuiLoadingSpinner, EuiDescriptionList, } from '@elastic/eui'; -import { AlertConsumers } from '@kbn/rule-data-utils'; +import { AlertConsumers, getEditRuleRoute, getRuleDetailsRoute } from '@kbn/rule-data-utils'; import { i18n } from '@kbn/i18n'; import { formatDuration } from '@kbn/alerting-plugin/common'; import { useLoadRuleTypesQuery } from '../../../hooks/use_load_rule_types_query'; import { RuleDefinitionProps } from '../../../../types'; import { RuleType } from '../../../..'; import { useKibana } from '../../../../common/lib/kibana'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { hasAllPrivilege, hasExecuteActionsCapability, @@ -38,11 +39,14 @@ export const RuleDefinition: React.FunctionComponent = ({ onEditRule, hideEditButton = false, filteredRuleTypes = [], + useNewRuleForm = false, }) => { const { - application: { capabilities }, + application: { capabilities, navigateToApp }, } = useKibana().services; + const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); + const [editFlyoutVisible, setEditFlyoutVisible] = useState(false); const [ruleType, setRuleType] = useState(); const { @@ -103,6 +107,20 @@ export const RuleDefinition: React.FunctionComponent = ({ return ''; }, [rule, ruleTypeRegistry]); + const onEditRuleClick = () => { + if (!isUsingRuleCreateFlyout && useNewRuleForm) { + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`, + state: { + returnApp: 'management', + returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`, + }, + }); + } else { + setEditFlyoutVisible(true); + } + }; + const ruleDefinitionList = [ { title: i18n.translate('xpack.triggersActionsUI.ruleDetails.ruleType', { @@ -153,7 +171,7 @@ export const RuleDefinition: React.FunctionComponent = ({ > {hasEditButton ? ( - setEditFlyoutVisible(true)} flush="left"> + {getRuleConditionsWording()} ) : ( @@ -206,7 +224,7 @@ export const RuleDefinition: React.FunctionComponent = ({ setEditFlyoutVisible(true)} + onClick={onEditRuleClick} /> ) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index 615efb5ed74b6..ffde171117385 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -23,6 +23,10 @@ import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); + jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config', () => ({ fetchUiConfig: jest .fn() diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index 8b2ee15db87d1..9422abdba3ec0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -26,7 +26,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { RuleExecutionStatusErrorReasons, parseDuration } from '@kbn/alerting-plugin/common'; -import { getRuleDetailsRoute } from '@kbn/rule-data-utils'; +import { getEditRuleRoute, getRuleDetailsRoute } from '@kbn/rule-data-utils'; import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config'; import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation'; import { bulkUpdateAPIKey } from '../../../lib/rule_api/update_api_key'; @@ -71,6 +71,7 @@ import { import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast'; import { RefreshToken } from './types'; import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; export type RuleDetailsProps = { rule: Rule; @@ -78,6 +79,7 @@ export type RuleDetailsProps = { actionTypes: ActionType[]; requestRefresh: () => Promise; refreshToken?: RefreshToken; + useNewRuleForm?: boolean; } & Pick< BulkOperationsComponentOpts, 'bulkDisableRules' | 'bulkEnableRules' | 'bulkDeleteRules' | 'snoozeRule' | 'unsnoozeRule' @@ -98,7 +100,7 @@ export const RuleDetails: React.FunctionComponent = ({ }) => { const history = useHistory(); const { - application: { capabilities }, + application: { capabilities, navigateToApp }, ruleTypeRegistry, actionTypeRegistry, setBreadcrumbs, @@ -108,6 +110,9 @@ export const RuleDetails: React.FunctionComponent = ({ theme, notifications: { toasts }, } = useKibana().services; + + const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); + const ruleReducer = useMemo(() => getRuleReducer(actionTypeRegistry), [actionTypeRegistry]); const [{}, dispatch] = useReducer(ruleReducer, { rule }); const setInitialRule = (value: Rule) => { @@ -206,7 +211,7 @@ export const RuleDetails: React.FunctionComponent = ({ data-test-subj="ruleIntervalToastEditButton" onClick={() => { toasts.remove(configurationToast); - setEditFlyoutVisibility(true); + onEditRuleClick(); }} > = ({ }); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ i18nStart, theme, @@ -256,12 +262,26 @@ export const RuleDetails: React.FunctionComponent = ({ } }; + const onEditRuleClick = () => { + if (!isUsingRuleCreateFlyout) { + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`, + state: { + returnApp: 'management', + returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`, + }, + }); + } else { + setEditFlyoutVisibility(true); + } + }; + const editButton = hasEditButton ? ( <> setEditFlyoutVisibility(true)} + onClick={onEditRuleClick} name="edit" disabled={!ruleType.enabledInLicense} > @@ -529,7 +549,7 @@ export const RuleDetails: React.FunctionComponent = ({ setEditFlyoutVisibility(true)} + onClick={onEditRuleClick} > ({ .fn() .mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false } }), })); + +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); + describe('rule_details_route', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx new file mode 100644 index 0000000000000..496b4d30873e6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { RuleForm } from '@kbn/alerts-ui-shared/src/rule_form/rule_form'; +import { getRuleDetailsRoute } from '@kbn/rule-data-utils'; +import { useLocation, useParams } from 'react-router-dom'; +import { useKibana } from '../../../common/lib/kibana'; +import { getAlertingSectionBreadcrumb } from '../../lib/breadcrumb'; +import { getCurrentDocTitle } from '../../lib/doc_title'; + +export const RuleFormRoute = () => { + const { + http, + i18n, + theme, + application, + notifications, + charts, + settings, + data, + dataViews, + unifiedSearch, + docLinks, + ruleTypeRegistry, + actionTypeRegistry, + chrome, + setBreadcrumbs, + } = useKibana().services; + + const location = useLocation<{ returnApp?: string; returnPath?: string }>(); + const { id, ruleTypeId } = useParams<{ + id?: string; + ruleTypeId?: string; + }>(); + const { returnApp, returnPath } = location.state || {}; + + // Set breadcrumb and page title + useEffect(() => { + if (id) { + setBreadcrumbs([ + getAlertingSectionBreadcrumb('rules', true), + getAlertingSectionBreadcrumb('editRule'), + ]); + chrome.docTitle.change(getCurrentDocTitle('editRule')); + } + if (ruleTypeId) { + setBreadcrumbs([ + getAlertingSectionBreadcrumb('rules', true), + getAlertingSectionBreadcrumb('createRule'), + ]); + chrome.docTitle.change(getCurrentDocTitle('createRule')); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + { + if (returnApp && returnPath) { + application.navigateToApp(returnApp, { path: returnPath }); + } else { + application.navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/rules`, + }); + } + }} + onSubmit={(ruleId) => { + application.navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(ruleId)}`, + }); + }} + /> + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { RuleFormRoute as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index a36068125a6a5..d98aa2c5dec67 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -45,6 +45,8 @@ import { RuleCreationValidConsumer, ruleDetailsRoute as commonRuleDetailsRoute, STACK_ALERTS_FEATURE_ID, + getCreateRuleRoute, + getEditRuleRoute, } from '@kbn/rule-data-utils'; import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared'; import { @@ -139,6 +141,7 @@ export interface RulesListProps { onRefresh?: (refresh: Date) => void; setHeaderActions?: (components?: React.ReactNode[]) => void; initialSelectedConsumer?: RuleCreationValidConsumer | null; + useNewRuleForm?: boolean; } export const percentileFields = { @@ -180,12 +183,13 @@ export const RulesList = ({ onRefresh, setHeaderActions, initialSelectedConsumer = STACK_ALERTS_FEATURE_ID, + useNewRuleForm = false, }: RulesListProps) => { const history = useHistory(); const kibanaServices = useKibana().services; const { actionTypeRegistry, - application: { capabilities }, + application: { capabilities, navigateToApp }, http, kibanaFeatures, notifications: { toasts }, @@ -211,6 +215,7 @@ export const RulesList = ({ const cloneRuleId = useRef(null); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); + const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); const [percentileOptions, setPercentileOptions] = useState(initialPercentileOptions); @@ -312,8 +317,18 @@ export const RulesList = ({ }); const onRuleEdit = (ruleItem: RuleTableItem) => { - setEditFlyoutVisibility(true); - setCurrentRuleToEdit(ruleItem); + if (!isUsingRuleCreateFlyout && useNewRuleForm) { + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(ruleItem.id)}`, + state: { + returnApp: 'management', + returnPath: `insightsAndAlerting/triggersActions/rules`, + }, + }); + } else { + setEditFlyoutVisibility(true); + setCurrentRuleToEdit(ruleItem); + } }; const onRunRule = async (id: string) => { @@ -1006,9 +1021,15 @@ export const RulesList = ({ setRuleTypeModalVisibility(false)} onSelectRuleType={(ruleTypeId) => { - setRuleTypeIdToCreate(ruleTypeId); - setRuleTypeModalVisibility(false); - setRuleFlyoutVisibility(true); + if (!isUsingRuleCreateFlyout) { + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getCreateRuleRoute(ruleTypeId)}`, + }); + } else { + setRuleTypeIdToCreate(ruleTypeId); + setRuleTypeModalVisibility(false); + setRuleFlyoutVisibility(true); + } }} http={http} toasts={toasts} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx index b7ffa1aa48b18..b33622423b92d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_experimental_features.test.tsx @@ -24,7 +24,7 @@ describe('getIsExperimentalFeatureEnabled', () => { ruleKqlBar: true, isMustacheAutocompleteOn: false, showMustacheAutocompleteSwitch: false, - ruleFormV2: false, + isUsingRuleCreateFlyout: false, }, }); @@ -64,7 +64,7 @@ describe('getIsExperimentalFeatureEnabled', () => { expect(result).toEqual(false); - result = getIsExperimentalFeatureEnabled('ruleFormV2'); + result = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); expect(result).toEqual(false); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 6ad86397606c8..a592b19ae8a79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -395,6 +395,7 @@ export interface RuleDefinitionProps Promise; hideEditButton?: boolean; filteredRuleTypes?: string[]; + useNewRuleForm?: boolean; } export enum Percentiles { diff --git a/x-pack/test/common/services/spaces.ts b/x-pack/test/common/services/spaces.ts index 98cc54e456200..67da912fb6a54 100644 --- a/x-pack/test/common/services/spaces.ts +++ b/x-pack/test/common/services/spaces.ts @@ -77,6 +77,25 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) { }; } + public async update( + id: string, + updatedSpace: Partial, + { overwrite = true }: { overwrite?: boolean } = {} + ) { + log.debug(`updating space ${id}`); + const { data, status, statusText } = await axios.put( + `/api/spaces/space/${id}?overwrite=${overwrite}`, + updatedSpace + ); + + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + log.debug(`updated space ${id}`); + } + public async delete(spaceId: string) { log.debug(`deleting space id: ${spaceId}`); const { data, status, statusText } = await axios.delete(`/api/spaces/space/${spaceId}`); @@ -89,6 +108,20 @@ export function SpacesServiceProvider({ getService }: FtrProviderContext) { log.debug(`deleted space id: ${spaceId}`); } + public async get(id: string) { + log.debug(`retrieving space ${id}`); + const { data, status, statusText } = await axios.get(`/api/spaces/space/${id}`); + + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + log.debug(`retrieved space ${id}`); + + return data; + } + public async getAll() { log.debug('retrieving all spaces'); const { data, status, statusText } = await axios.get('/api/spaces/space'); diff --git a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/index.ts b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/index.ts index 99ce8f2ab16e7..45a8f78387154 100644 --- a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/index.ts +++ b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function spacesApp({ loadTestFile }: FtrProviderContext) { describe('Spaces app (with solution view)', function spacesAppTestSuite() { loadTestFile(require.resolve('./create_edit_space')); + loadTestFile(require.resolve('./solution_tour')); }); } diff --git a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/solution_tour.ts b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/solution_tour.ts new file mode 100644 index 0000000000000..852a2a83031cd --- /dev/null +++ b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/solution_tour.ts @@ -0,0 +1,133 @@ +/* + * 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 expect from '@kbn/expect'; +import type { SolutionView, Space } from '@kbn/spaces-plugin/common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const spacesService = getService('spaces'); + const browser = getService('browser'); + const es = getService('es'); + const log = getService('log'); + + describe('space solution tour', () => { + let version: string | undefined; + + const removeGlobalSettings = async () => { + version = version ?? (await kibanaServer.version.get()); + version = version.replace(/-SNAPSHOT$/, ''); + + log.debug(`Deleting [config-global:${version}] doc from the .kibana index`); + + await es + .delete( + { id: `config-global:${version}`, index: '.kibana', refresh: true }, + { headers: { 'kbn-xsrf': 'spaces' } } + ) + .catch((error) => { + if (error.statusCode === 404) return; // ignore 404 errors + throw error; + }); + }; + + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); + + describe('solution tour', () => { + let _defaultSpace: Space | undefined = { + id: 'default', + name: 'Default', + disabledFeatures: [], + }; + + const updateSolutionDefaultSpace = async (solution: SolutionView) => { + log.debug(`Updating default space solution: [${solution}].`); + + await spacesService.update('default', { + ..._defaultSpace, + solution, + }); + }; + + before(async () => { + _defaultSpace = await spacesService.get('default'); + + await PageObjects.common.navigateToUrl('management', 'kibana/spaces', { + shouldUseHashForSubUrl: false, + }); + + await PageObjects.common.sleep(1000); // wait to save the setting + }); + + afterEach(async () => { + await updateSolutionDefaultSpace('classic'); // revert to not impact future tests + }); + + it('does not show the solution tour for the classic space', async () => { + await testSubjects.missingOrFail('spaceSolutionTour', { timeout: 3000 }); + }); + + it('does show the solution tour if the default space has a solution set', async () => { + await updateSolutionDefaultSpace('es'); // set a solution + await PageObjects.common.sleep(500); + await removeGlobalSettings(); // Make sure we start from a clean state + await browser.refresh(); + + await testSubjects.existOrFail('spaceSolutionTour', { timeout: 3000 }); + + await testSubjects.click('closeTourBtn'); // close the tour + await PageObjects.common.sleep(1000); // wait to save the setting + + await browser.refresh(); + await testSubjects.missingOrFail('spaceSolutionTour', { timeout: 3000 }); // The tour does not appear after refresh + }); + + it('does not show the solution tour after updating the default space from classic to solution', async () => { + await updateSolutionDefaultSpace('es'); // set a solution + await PageObjects.common.sleep(500); + await browser.refresh(); + + // The tour does not appear after refresh, even with the default space with a solution set + await testSubjects.missingOrFail('spaceSolutionTour', { timeout: 3000 }); + }); + + it('does not show the solution tour after deleting spaces and leave only the default', async () => { + await updateSolutionDefaultSpace('es'); // set a solution + + await spacesService.create({ + id: 'foo-space', + name: 'Foo Space', + disabledFeatures: [], + color: '#AABBCC', + }); + + const allSpaces = await spacesService.getAll(); + expect(allSpaces).to.have.length(2); // Make sure we have 2 spaces + + await removeGlobalSettings(); // Make sure we start from a clean state + await browser.refresh(); + + await testSubjects.missingOrFail('spaceSolutionTour', { timeout: 3000 }); + + await spacesService.delete('foo-space'); + await browser.refresh(); + + // The tour still does not appear after refresh, even with 1 space with a solution set + await testSubjects.missingOrFail('spaceSolutionTour', { timeout: 3000 }); + }); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/entity/user_risk/data.json b/x-pack/test/functional/es_archives/entity/user_risk/data.json deleted file mode 100644 index 39b403deddc69..0000000000000 --- a/x-pack/test/functional/es_archives/entity/user_risk/data.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "type": "doc", - "value": { - "index": "ml_user_risk_score_latest_default", - "id": "1", - "source": { - "user": { - "name": "root", - "risk": { - "calculated_score_norm": 11, - "calculated_level": "Low" - } - }, - "ingest_timestamp": "2022-08-15T16:32:16.142561766Z", - "@timestamp": "2022-08-12T14:45:36.171Z" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "2", - "index": "ml_user_risk_score_latest_default", - "source": { - "host": { - "name": "User name 1", - "risk": { - "calculated_score_norm": 20, - "calculated_level": "Low" - } - }, - "ingest_timestamp": "2022-08-15T16:32:16.142561766Z", - "@timestamp": "2022-08-12T14:45:36.171Z" - }, - "type": "_doc" - } -} diff --git a/x-pack/test/functional/es_archives/entity/user_risk/mappings.json b/x-pack/test/functional/es_archives/entity/user_risk/mappings.json deleted file mode 100644 index 22518c9c455fc..0000000000000 --- a/x-pack/test/functional/es_archives/entity/user_risk/mappings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - - "type": "index", - "value": { - "index": "ml_user_risk_score_latest_default", - "mappings": { - "properties": { - "user": { - "properties": { - "name": { - "type": "keyword" - }, - "risk": { - "properties": { - "calculated_level": { - "type": "keyword" - }, - "calculated_score_norm": { - "type": "float" - } - } - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional_with_es_ssl/config.base.ts b/x-pack/test/functional_with_es_ssl/config.base.ts index 2fdf49bc41fef..b4cc8a734a270 100644 --- a/x-pack/test/functional_with_es_ssl/config.base.ts +++ b/x-pack/test/functional_with_es_ssl/config.base.ts @@ -85,6 +85,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { 'stackAlertsPage', 'ruleTagFilter', 'ruleStatusFilter', + 'isUsingRuleCreateFlyout', ])}`, `--xpack.alerting.rules.minimumScheduleInterval.value="2s"`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, diff --git a/x-pack/test_serverless/functional/config.base.ts b/x-pack/test_serverless/functional/config.base.ts index b0cd556fe1d36..1a3cd2ffd6a5b 100644 --- a/x-pack/test_serverless/functional/config.base.ts +++ b/x-pack/test_serverless/functional/config.base.ts @@ -36,6 +36,11 @@ export function createTestConfig(options: CreateTestConfigOptions) { serverArgs: [ ...svlSharedConfig.get('kbnTestServer.serverArgs'), `--serverless=${options.serverlessProject}`, + // Ensures the existing E2E tests are backwards compatible with the old rule create flyout + // Remove this experiment once all of the migration has been completed + `--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([ + 'isUsingRuleCreateFlyout', + ])}`, // custom native roles are enabled only for search and security projects ...(options.serverlessProject !== 'oblt' ? ['--xpack.security.roleManagementEnabled=true'] diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts index b26581fb46dfd..8164fd39a81de 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts @@ -25,6 +25,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const AWS_SINGLE_ACCOUNT_TEST_ID = 'awsSingleTestId'; describe('Agentless API Serverless', function () { + // fails on MKI, see https://github.com/elastic/kibana/issues/196245 + this.tags(['failsOnMKI']); + let mockApiServer: http.Server; let cisIntegration: typeof pageObjects.cisAddIntegration;