diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc index 9d254ce5fc834..62604c465dafc 100644 --- a/docs/user/alerting/alerting-troubleshooting.asciidoc +++ b/docs/user/alerting/alerting-troubleshooting.asciidoc @@ -83,12 +83,11 @@ The result of this HTTP request (and printed to stdout by https://github.com/pmu [[alerting-error-banners]] === Look for error banners -The *Rule Management* and *Rule Details* pages contain an error banner, which helps to identify the errors for the rules: -[role="screenshot"] -image::images/rules-management-health.png[Rule management page with the errors banner] +The **{stack-manage-app}** > *{rules-ui}* page contains an error banner that +helps to identify the errors for the rules: [role="screenshot"] -image::images/rules-details-health.png[Rule details page with the errors banner] +image::images/rules-management-health.png[Rule management page with the errors banner] [float] [[task-manager-diagnostics]] diff --git a/docs/user/alerting/images/rules-details-health.png b/docs/user/alerting/images/rules-details-health.png deleted file mode 100644 index ffdac4fcd1983..0000000000000 Binary files a/docs/user/alerting/images/rules-details-health.png and /dev/null differ diff --git a/docs/user/alerting/images/rules-management-health.png b/docs/user/alerting/images/rules-management-health.png index e81c4e07dd7b2..edd16b245ec65 100644 Binary files a/docs/user/alerting/images/rules-management-health.png and b/docs/user/alerting/images/rules-management-health.png differ diff --git a/examples/field_formats_example/tsconfig.json b/examples/field_formats_example/tsconfig.json index 66e9d7db028c7..a7651b649e5b3 100644 --- a/examples/field_formats_example/tsconfig.json +++ b/examples/field_formats_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/files_example/tsconfig.json b/examples/files_example/tsconfig.json index 2ce0ddb8f7d66..9329f941c1006 100644 --- a/examples/files_example/tsconfig.json +++ b/examples/files_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target/types" }, diff --git a/examples/hello_world/tsconfig.json b/examples/hello_world/tsconfig.json index f074171954048..6cfb28f7b3317 100644 --- a/examples/hello_world/tsconfig.json +++ b/examples/hello_world/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target/types" }, diff --git a/examples/partial_results_example/tsconfig.json b/examples/partial_results_example/tsconfig.json index ba03cbc836189..97d4c752cc3b5 100644 --- a/examples/partial_results_example/tsconfig.json +++ b/examples/partial_results_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts index ac1bcb02a0349..3e9e25e790b47 100644 --- a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts +++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts @@ -79,7 +79,7 @@ export class BundleMetricsPlugin { id: bundle.id, value: entry.size, limit: bundle.pageLoadAssetSizeLimit, - limitConfigPath: `node_modules/@kbn/optimizer/limits.yml`, + limitConfigPath: `packages/kbn-optimizer/limits.yml`, }, { group: `async chunks size`, diff --git a/src/dev/typescript/project.ts b/src/dev/typescript/project.ts index 32245e26c69ec..c148cccfa7351 100644 --- a/src/dev/typescript/project.ts +++ b/src/dev/typescript/project.ts @@ -174,4 +174,8 @@ export class Project { ? [this.tsConfigPath, ...this.baseProject.getConfigPaths()] : [this.tsConfigPath]; } + + public getProjectsDeep(): Project[] { + return this.baseProject ? [this, ...this.baseProject.getProjectsDeep()] : [this]; + } } diff --git a/src/dev/typescript/run_check_ts_projects_cli.ts b/src/dev/typescript/run_check_ts_projects_cli.ts index 9156c52a23d01..c4998e6791957 100644 --- a/src/dev/typescript/run_check_ts_projects_cli.ts +++ b/src/dev/typescript/run_check_ts_projects_cli.ts @@ -12,6 +12,7 @@ import { run } from '@kbn/dev-cli-runner'; import { asyncMapWithLimit } from '@kbn/std'; import { createFailError } from '@kbn/dev-cli-errors'; import { getRepoFiles } from '@kbn/get-repo-files'; +import { REPO_ROOT } from '@kbn/utils'; import globby from 'globby'; import { File } from '../file'; @@ -37,6 +38,25 @@ export async function runCheckTsProjectsCli() { const stats = new Stats(); let failed = false; + const everyProjectDeep = new Set(PROJECTS.flatMap((p) => p.getProjectsDeep())); + for (const proj of everyProjectDeep) { + const [, ...baseConfigRels] = proj.getConfigPaths().map((p) => Path.relative(REPO_ROOT, p)); + const configRel = Path.relative(REPO_ROOT, proj.tsConfigPath); + + if (baseConfigRels[0] === 'tsconfig.json') { + failed = true; + log.error( + `[${configRel}]: This tsconfig extends the root tsconfig.json file and shouldn't. The root tsconfig.json file is not a valid base config, you probably want to point to the tsconfig.base.json file.` + ); + } + if (configRel !== 'tsconfig.base.json' && !baseConfigRels.includes('tsconfig.base.json')) { + failed = true; + log.error( + `[${configRel}]: This tsconfig does not extend the tsconfig.base.json file either directly or indirectly. The TS config setup for the repo expects every tsconfig file to extend this base config file.` + ); + } + } + const pathsAndProjects = await asyncMapWithLimit(PROJECTS, 5, async (proj) => { const paths = await globby(proj.getIncludePatterns(), { ignore: proj.getExcludePatterns(), diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap index 508d8aac06dad..a154ededc2a33 100644 --- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap @@ -1040,6 +1040,72 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the }, "indices": { "properties": { + "metric": { + "properties": { + "shards": { + "properties": { + "total": { + "type": "long" + } + } + }, + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "traces": { + "properties": { + "shards": { + "properties": { + "total": { + "type": "long" + } + } + }, + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, "shards": { "properties": { "total": { @@ -1086,6 +1152,12 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "service_id": { "type": "keyword" }, + "num_service_nodes": { + "type": "long" + }, + "num_transaction_types": { + "type": "long" + }, "timed_out": { "type": "boolean" }, diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx index ac7c38dc2f888..8a90a1cffb890 100644 --- a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx @@ -55,8 +55,7 @@ export function IconPopover({ ownFocus={false} button={ { const task = tasks.find((t) => t.name === 'indices_stats'); it('returns a map of index stats', async () => { - const indicesStats = jest.fn().mockResolvedValueOnce({ + const indicesStats = jest.fn().mockResolvedValue({ _all: { total: { docs: { count: 1 }, store: { size_in_bytes: 1 } } }, _shards: { total: 1 }, }); + const statsResponse = { + shards: { + total: 1, + }, + all: { + total: { + docs: { + count: 1, + }, + store: { + size_in_bytes: 1, + }, + }, + }, + }; + expect( await task?.executor({ indices, @@ -395,26 +411,32 @@ describe('data telemetry collection tasks', () => { } as any) ).toEqual({ indices: { + ...statsResponse, + metric: statsResponse, + traces: statsResponse, + }, + }); + }); + + describe('with no results', () => { + it('returns zero values', async () => { + const indicesStats = jest.fn().mockResolvedValue({}); + + const statsResponse = { shards: { - total: 1, + total: 0, }, all: { total: { docs: { - count: 1, + count: 0, }, store: { - size_in_bytes: 1, + size_in_bytes: 0, }, }, }, - }, - }); - }); - - describe('with no results', () => { - it('returns zero values', async () => { - const indicesStats = jest.fn().mockResolvedValueOnce({}); + }; expect( await task?.executor({ @@ -423,19 +445,9 @@ describe('data telemetry collection tasks', () => { } as any) ).toEqual({ indices: { - shards: { - total: 0, - }, - all: { - total: { - docs: { - count: 0, - }, - store: { - size_in_bytes: 0, - }, - }, - }, + ...statsResponse, + metric: statsResponse, + traces: statsResponse, }, }); }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index 34d434df654ad..df68cb4155b24 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -4,20 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { fromKueryExpression } from '@kbn/es-query'; -import { flatten, merge, sortBy, sum, pickBy, uniq } from 'lodash'; -import { createHash } from 'crypto'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { fromKueryExpression } from '@kbn/es-query'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { createHash } from 'crypto'; +import { flatten, merge, pickBy, sortBy, sum, uniq } from 'lodash'; import { TelemetryTask } from '.'; import { AGENT_NAMES, RUM_AGENT_NAMES } from '../../../../common/agent_name'; -import { - SavedServiceGroup, - APM_SERVICE_GROUP_SAVED_OBJECT_TYPE, - MAX_NUMBER_OF_SERVICE_GROUPS, -} from '../../../../common/service_groups'; -import { getKueryFields } from '../../../../common/utils/get_kuery_fields'; import { AGENT_NAME, AGENT_VERSION, @@ -30,9 +23,9 @@ import { FAAS_TRIGGER_TYPE, HOST_NAME, HOST_OS_PLATFORM, + KUBERNETES_POD_NAME, OBSERVER_HOSTNAME, PARENT_ID, - KUBERNETES_POD_NAME, PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_FRAMEWORK_NAME, @@ -40,6 +33,7 @@ import { SERVICE_LANGUAGE_NAME, SERVICE_LANGUAGE_VERSION, SERVICE_NAME, + SERVICE_NODE_NAME, SERVICE_RUNTIME_NAME, SERVICE_RUNTIME_VERSION, SERVICE_VERSION, @@ -48,11 +42,18 @@ import { TRANSACTION_TYPE, USER_AGENT_ORIGINAL, } from '../../../../common/elasticsearch_fieldnames'; +import { + APM_SERVICE_GROUP_SAVED_OBJECT_TYPE, + MAX_NUMBER_OF_SERVICE_GROUPS, + SavedServiceGroup, +} from '../../../../common/service_groups'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { getKueryFields } from '../../../../common/utils/get_kuery_fields'; import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { Span } from '../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { APMTelemetry, APMPerService } from '../types'; +import { APMPerService, APMTelemetry } from '../types'; const TIME_RANGES = ['1d', 'all'] as const; type TimeRange = typeof TIME_RANGES[number]; @@ -1027,8 +1028,50 @@ export const tasks: TelemetryTask[] = [ ], }); + const metricIndicesResponse = await telemetryClient.indicesStats({ + index: [indices.metric], + }); + + const tracesIndicesResponse = await telemetryClient.indicesStats({ + index: [indices.span, indices.transaction], + }); + return { indices: { + metric: { + shards: { + total: metricIndicesResponse._shards?.total ?? 0, + }, + all: { + total: { + docs: { + count: metricIndicesResponse._all?.total?.docs?.count ?? 0, + }, + store: { + size_in_bytes: + metricIndicesResponse._all?.total?.store?.size_in_bytes ?? + 0, + }, + }, + }, + }, + traces: { + shards: { + total: tracesIndicesResponse._shards?.total ?? 0, + }, + all: { + total: { + docs: { + count: tracesIndicesResponse._all?.total?.docs?.count ?? 0, + }, + store: { + size_in_bytes: + tracesIndicesResponse._all?.total?.store?.size_in_bytes ?? + 0, + }, + }, + }, + }, shards: { total: response._shards?.total ?? 0, }, @@ -1193,18 +1236,28 @@ export const tasks: TelemetryTask[] = [ }, }, aggs: { - environments: { + service_names: { terms: { - field: SERVICE_ENVIRONMENT, - size: 1000, + field: SERVICE_NAME, + size: 2500, }, aggs: { - service_names: { + environments: { terms: { - field: SERVICE_NAME, - size: 1000, + field: SERVICE_ENVIRONMENT, + size: 5, }, aggs: { + instances: { + cardinality: { + field: SERVICE_NODE_NAME, + }, + }, + transaction_types: { + cardinality: { + field: TRANSACTION_TYPE, + }, + }, top_metrics: { top_metrics: { sort: '_score', @@ -1273,87 +1326,85 @@ export const tasks: TelemetryTask[] = [ }, }, }); - const envBuckets = response.aggregations?.environments.buckets ?? []; - const data: APMPerService[] = envBuckets.flatMap((envBucket) => { + const serviceBuckets = response.aggregations?.service_names.buckets ?? []; + const data: APMPerService[] = serviceBuckets.flatMap((serviceBucket) => { const envHash = createHash('sha256') - .update(envBucket.key as string) + .update(serviceBucket.key as string) .digest('hex'); - const serviceBuckets = envBucket.service_names?.buckets ?? []; - return serviceBuckets.map((serviceBucket) => { + const envBuckets = serviceBucket.environments?.buckets ?? []; + return envBuckets.map((envBucket) => { const nameHash = createHash('sha256') - .update(serviceBucket.key as string) + .update(envBucket.key as string) .digest('hex'); const fullServiceName = `${nameHash}~${envHash}`; return { service_id: fullServiceName, timed_out: response.timed_out, + num_service_nodes: envBucket.instances.value ?? 1, + num_transaction_types: envBucket.transaction_types.value ?? 0, cloud: { availability_zones: - serviceBucket[CLOUD_AVAILABILITY_ZONE]?.buckets.map( + envBucket[CLOUD_AVAILABILITY_ZONE]?.buckets.map( (inner) => inner.key as string ) ?? [], regions: - serviceBucket[CLOUD_REGION]?.buckets.map( + envBucket[CLOUD_REGION]?.buckets.map( (inner) => inner.key as string ) ?? [], providers: - serviceBucket[CLOUD_PROVIDER]?.buckets.map( + envBucket[CLOUD_PROVIDER]?.buckets.map( (inner) => inner.key as string ) ?? [], }, faas: { trigger: { type: - serviceBucket[FAAS_TRIGGER_TYPE]?.buckets.map( + envBucket[FAAS_TRIGGER_TYPE]?.buckets.map( (inner) => inner.key as string ) ?? [], }, }, agent: { - name: serviceBucket.top_metrics?.top[0].metrics[ - AGENT_NAME - ] as string, - version: serviceBucket.top_metrics?.top[0].metrics[ + name: envBucket.top_metrics?.top[0].metrics[AGENT_NAME] as string, + version: envBucket.top_metrics?.top[0].metrics[ AGENT_VERSION ] as string, }, service: { language: { - name: serviceBucket.top_metrics?.top[0].metrics[ + name: envBucket.top_metrics?.top[0].metrics[ SERVICE_LANGUAGE_NAME ] as string, - version: serviceBucket.top_metrics?.top[0].metrics[ + version: envBucket.top_metrics?.top[0].metrics[ SERVICE_LANGUAGE_VERSION ] as string, }, framework: { - name: serviceBucket.top_metrics?.top[0].metrics[ + name: envBucket.top_metrics?.top[0].metrics[ SERVICE_FRAMEWORK_NAME ] as string, - version: serviceBucket.top_metrics?.top[0].metrics[ + version: envBucket.top_metrics?.top[0].metrics[ SERVICE_FRAMEWORK_VERSION ] as string, }, runtime: { - name: serviceBucket.top_metrics?.top[0].metrics[ + name: envBucket.top_metrics?.top[0].metrics[ SERVICE_RUNTIME_NAME ] as string, - version: serviceBucket.top_metrics?.top[0].metrics[ + version: envBucket.top_metrics?.top[0].metrics[ SERVICE_RUNTIME_VERSION ] as string, }, }, kubernetes: { pod: { - name: serviceBucket.top_metrics?.top[0].metrics[ + name: envBucket.top_metrics?.top[0].metrics[ KUBERNETES_POD_NAME ] as string, }, }, container: { - id: serviceBucket.top_metrics?.top[0].metrics[ - CONTAINER_ID - ] as string, + id: envBucket.top_metrics?.top[0].metrics[CONTAINER_ID] as string, }, }; }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts index 4430389785e1d..c73eeae128cfe 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts @@ -118,6 +118,8 @@ const apmPerAgentSchema: Pick< export const apmPerServiceSchema: MakeSchemaFrom = { service_id: keyword, + num_service_nodes: long, + num_transaction_types: long, timed_out: { type: 'boolean' }, cloud: { availability_zones: { type: 'array', items: { type: 'keyword' } }, @@ -225,6 +227,24 @@ export const apmSchema: MakeSchemaFrom = { integrations: { ml: { all_jobs_count: long } }, indices: { + metric: { + shards: { total: long }, + all: { + total: { + docs: { count: long }, + store: { size_in_bytes: long }, + }, + }, + }, + traces: { + shards: { total: long }, + all: { + total: { + docs: { count: long }, + store: { size_in_bytes: long }, + }, + }, + }, shards: { total: long }, all: { total: { diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts index ceadcbfc1ded2..518bb969bcb0e 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts @@ -27,6 +27,8 @@ export interface AggregatedTransactionsCounts { export interface APMPerService { service_id: string; timed_out: boolean; + num_service_nodes: number; + num_transaction_types: number; cloud: { availability_zones: string[]; regions: string[]; @@ -157,6 +159,36 @@ export interface APMUsage { } >; indices: { + traces: { + shards: { + total: number; + }; + all: { + total: { + docs: { + count: number; + }; + store: { + size_in_bytes: number; + }; + }; + }; + }; + metric: { + shards: { + total: number; + }; + all: { + total: { + docs: { + count: number; + }; + store: { + size_in_bytes: number; + }; + }; + }; + }; shards: { total: number; }; diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 052f3cb743d37..a78266a51f279 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -8,7 +8,6 @@ export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status'; export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats'; export const BENCHMARKS_ROUTE_PATH = '/internal/cloud_security_posture/benchmarks'; -export const ES_PIT_ROUTE_PATH = '/internal/cloud_security_posture/es_pit'; export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture'; @@ -47,11 +46,3 @@ export const CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE = 'csp-rule-template'; export const CLOUDBEAT_VANILLA = 'cloudbeat/cis_k8s'; // Integration input export const CLOUDBEAT_EKS = 'cloudbeat/cis_eks'; // Integration input - -export const LOCAL_STORAGE_PAGE_SIZE_LATEST_FINDINGS_KEY = 'cloudPosture:latestFindings:pageSize'; -export const LOCAL_STORAGE_PAGE_SIZE_RESOURCE_FINDINGS_KEY = - 'cloudPosture:resourceFindings:pageSize'; -export const LOCAL_STORAGE_PAGE_SIZE_FINDINGS_BY_RESOURCE_KEY = - 'cloudPosture:findingsByResource:pageSize'; -export const LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY = 'cloudPosture:benchmark:pageSize'; -export const LOCAL_STORAGE_PAGE_SIZE_RULES_KEY = 'cloudPosture:rules:pageSize'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 6356f00b5ec9f..15f1e890cd672 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -15,6 +15,12 @@ export const statusColors = { }; export const CSP_MOMENT_FORMAT = 'MMMM D, YYYY @ HH:mm:ss.SSS'; +export const MAX_FINDINGS_TO_LOAD = 500; +export const DEFAULT_VISIBLE_ROWS_PER_PAGE = 25; + +export const LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY = 'cloudPosture:findings:pageSize'; +export const LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY = 'cloudPosture:benchmark:pageSize'; +export const LOCAL_STORAGE_PAGE_SIZE_RULES_KEY = 'cloudPosture:rules:pageSize'; export type CloudPostureIntegrations = typeof cloudPostureIntegrations; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_page_size.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_page_size.ts new file mode 100644 index 0000000000000..314dfbe661d93 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_page_size.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 useLocalStorage from 'react-use/lib/useLocalStorage'; +import { DEFAULT_VISIBLE_ROWS_PER_PAGE } from '../constants'; + +/** + * @description handles persisting the users table row size selection + */ +export const usePageSize = (localStorageKey: string) => { + const [persistedPageSize, setPersistedPageSize] = useLocalStorage( + localStorageKey, + DEFAULT_VISIBLE_ROWS_PER_PAGE + ); + + let pageSize: number = DEFAULT_VISIBLE_ROWS_PER_PAGE; + + if (persistedPageSize) { + pageSize = persistedPageSize; + } + + return { pageSize, setPageSize: setPersistedPageSize }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_page_slice.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_page_slice.ts new file mode 100644 index 0000000000000..e089724b25909 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_page_slice.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 { useMemo } from 'react'; + +/** + * @description given an array index and page size, returns a slice of said array. + */ +export const usePageSlice = (data: any[] | undefined, pageIndex: number, pageSize: number) => { + return useMemo(() => { + if (!data) { + return []; + } + + const cursor = pageIndex * pageSize; + return data.slice(cursor, cursor + pageSize); + }, [data, pageIndex, pageSize]); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx index 12b26d38ac7c0..89fc1c14a93e8 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx @@ -77,7 +77,7 @@ const Indexing = () => (

} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx index ce615733e4b98..29bc94dd739ec 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx @@ -6,7 +6,6 @@ */ import React, { useState } from 'react'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; import { EuiFieldSearch, EuiFieldSearchProps, @@ -31,7 +30,8 @@ import { } from './use_csp_benchmark_integrations'; import { extractErrorMessage } from '../../../common/utils/helpers'; import * as TEST_SUBJ from './test_subjects'; -import { LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY } from '../../../common/constants'; +import { LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY } from '../../common/constants'; +import { usePageSize } from '../../common/hooks/use_page_size'; const SEARCH_DEBOUNCE_MS = 300; @@ -128,14 +128,11 @@ const BenchmarkSearchField = ({ }; export const Benchmarks = () => { - const [pageSize, setPageSize] = useLocalStorage( - LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY, - 10 - ); + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY); const [query, setQuery] = useState({ name: '', page: 1, - perPage: pageSize || 10, + perPage: pageSize, sortField: 'package_policy.name', sortOrder: 'asc', }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/es_pit/findings_es_pit_context.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/es_pit/findings_es_pit_context.ts deleted file mode 100644 index aa1d660229d75..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/es_pit/findings_es_pit_context.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createContext, type MutableRefObject } from 'react'; -import type { UseQueryResult } from '@tanstack/react-query'; - -interface FindingsEsPitContextValue { - setPitId(newPitId: string): void; - pitIdRef: MutableRefObject; - pitQuery: UseQueryResult; -} - -// Default value should never be used, it can not be instantiated statically. Always wrap in a provider with a value -export const FindingsEsPitContext = createContext( - {} as FindingsEsPitContextValue -); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/es_pit/use_findings_es_pit.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/es_pit/use_findings_es_pit.ts deleted file mode 100644 index d8f2b22f501d4..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/es_pit/use_findings_es_pit.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useRef, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { CSP_LATEST_FINDINGS_DATA_VIEW, ES_PIT_ROUTE_PATH } from '../../../../common/constants'; -import { useKibana } from '../../../common/hooks/use_kibana'; -import { FINDINGS_PIT_KEEP_ALIVE } from '../constants'; - -export const useFindingsEsPit = (queryKey: string) => { - // Using a reference for the PIT ID to avoid re-rendering when it changes - const pitIdRef = useRef(); - // Using this state as an internal control to ensure we run the query to open the PIT once and only once - const [isPitIdSet, setPitIdSet] = useState(false); - const setPitId = useCallback( - (newPitId: string) => { - pitIdRef.current = newPitId; - setPitIdSet(true); - }, - [pitIdRef, setPitIdSet] - ); - - const { http } = useKibana().services; - const pitQuery = useQuery( - ['findingsPitQuery', queryKey], - () => - http.post(ES_PIT_ROUTE_PATH, { - query: { index_name: CSP_LATEST_FINDINGS_DATA_VIEW, keep_alive: FINDINGS_PIT_KEEP_ALIVE }, - }), - { - enabled: !isPitIdSet, - onSuccess: (pitId) => { - setPitId(pitId); - }, - cacheTime: 0, - } - ); - - return { pitIdRef, setPitId, pitQuery }; -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx index 8e330abc93539..d0744239a975b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx @@ -26,7 +26,6 @@ import { useCISIntegrationPoliciesLink } from '../../common/navigation/use_navig import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration'; import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects'; import { render } from '@testing-library/react'; -import { useFindingsEsPit } from './es_pit/use_findings_es_pit'; import { expectIdsInDoc } from '../../test/utils'; import { fleetMock } from '@kbn/fleet-plugin/public/mocks'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; @@ -36,19 +35,10 @@ jest.mock('../../common/api/use_setup_status_api'); jest.mock('../../common/hooks/use_subscription_status'); jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies'); jest.mock('../../common/navigation/use_navigate_to_cis_integration'); -jest.mock('./es_pit/use_findings_es_pit'); const chance = new Chance(); beforeEach(() => { jest.restoreAllMocks(); - (useFindingsEsPit as jest.Mock).mockImplementation(() => ({ - pitQuery: createReactQueryResponse({ - status: 'success', - data: [], - }), - setPitId: () => {}, - pitIdRef: chance.guid(), - })); (useSubscriptionStatus as jest.Mock).mockImplementation(() => createReactQueryResponse({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx index e7aeed2e3e837..8830724193e1c 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx @@ -5,14 +5,11 @@ * 2.0. */ import React from 'react'; -import type { UseQueryResult } from '@tanstack/react-query'; import { Redirect, Switch, Route, useLocation } from 'react-router-dom'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { NoFindingsStates } from '../../components/no_findings_states'; import { CloudPosturePage } from '../../components/cloud_posture_page'; -import { useFindingsEsPit } from './es_pit/use_findings_es_pit'; -import { FindingsEsPitContext } from './es_pit/findings_es_pit_context'; import { useLatestFindingsDataView } from '../../common/api/use_latest_findings_data_view'; import { cloudPosturePages, findingsNavigation } from '../../common/navigation/constants'; import { FindingsByResourceContainer } from './latest_findings_by_resource/findings_by_resource_container'; @@ -21,59 +18,43 @@ import { LatestFindingsContainer } from './latest_findings/latest_findings_conta export const Findings = () => { const location = useLocation(); const dataViewQuery = useLatestFindingsDataView(); - // TODO: Consider splitting the PIT window so that each "group by" view has its own PIT - const { pitQuery, pitIdRef, setPitId } = useFindingsEsPit('findings'); const getSetupStatus = useCspSetupStatusApi(); const hasFindings = getSetupStatus.data?.status === 'indexed'; if (!hasFindings) return ; - let queryForCloudPosturePage: UseQueryResult = dataViewQuery; - if (pitQuery.isError || pitQuery.isLoading) { - queryForCloudPosturePage = pitQuery; - } - return ( - - , - setPitId, - }} - > - - ( - - )} - /> - ( - - - - )} - /> - } - /> - } - /> - - + + + ( + + )} + /> + ( + + + + )} + /> + } + /> + } + /> + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx index 5f5ee2e5a7ecf..2d709433e7fc5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx @@ -4,13 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { UseQueryResult } from '@tanstack/react-query'; -import { createReactQueryResponse } from '../../../test/fixtures/react_query'; import React from 'react'; import { render } from '@testing-library/react'; import { LatestFindingsContainer, getDefaultQuery } from './latest_findings_container'; import { createStubDataView } from '@kbn/data-views-plugin/common/mocks'; import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; +import { DEFAULT_VISIBLE_ROWS_PER_PAGE } from '../../../common/constants'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { TestProvider } from '../../../test/test_provider'; @@ -20,7 +19,6 @@ import { useLocation } from 'react-router-dom'; import { RisonObject } from 'rison-node'; import { buildEsQuery } from '@kbn/es-query'; import { getPaginationQuery } from '../utils/utils'; -import { FindingsEsPitContext } from '../es_pit/findings_es_pit_context'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks'; import { fleetMock } from '@kbn/fleet-plugin/public/mocks'; @@ -45,6 +43,7 @@ describe('', () => { filters: [], query: { language: 'kuery', query: '' }, }); + const pageSize = DEFAULT_VISIBLE_ROWS_PER_PAGE; const dataMock = dataPluginMock.createStartContract(); const dataView = createStubDataView({ spec: { @@ -56,13 +55,6 @@ describe('', () => { search: encodeQuery(query as unknown as RisonObject), }); - const setPitId = jest.fn(); - const pitIdRef = { current: '' }; - const pitQuery = createReactQueryResponse({ - status: 'success', - data: '', - }) as UseQueryResult; - render( ', () => { licensing: licensingMock.createStart(), }} > - - - + ); const baseQuery = { query: buildEsQuery(dataView, query.query, query.filters), - pitId: pitIdRef.current, }; expect(dataMock.search.search).toHaveBeenNthCalledWith(1, { params: getFindingsQuery({ ...baseQuery, - ...getPaginationQuery(query), + ...getPaginationQuery({ ...query, pageSize }), sort: query.sort, enabled: true, }), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx index 92b6ca6dcc68e..2aa13f19444ae 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx @@ -8,7 +8,6 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiBottomBar, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; import type { Evaluation } from '../../../../common/types'; import { CloudPosturePageTitle } from '../../../components/cloud_posture_page_title'; import type { FindingsBaseProps } from '../types'; @@ -22,7 +21,6 @@ import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; import { getFindingsPageSizeInfo, getFilters, - getPaginationQuery, getPaginationTableParams, useBaseEsQuery, usePersistedQuery, @@ -30,9 +28,14 @@ import { import { PageTitle, PageTitleText } from '../layout/findings_layout'; import { FindingsGroupBySelector } from '../layout/findings_group_by_selector'; import { useUrlQuery } from '../../../common/hooks/use_url_query'; +import { usePageSlice } from '../../../common/hooks/use_page_slice'; +import { usePageSize } from '../../../common/hooks/use_page_size'; import { ErrorCallout } from '../layout/error_callout'; import { getLimitProperties } from '../utils/get_limit_properties'; -import { LOCAL_STORAGE_PAGE_SIZE_LATEST_FINDINGS_KEY } from '../../../../common/constants'; +import { + MAX_FINDINGS_TO_LOAD, + LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY, +} from '../../../common/constants'; export const getDefaultQuery = ({ query, @@ -42,18 +45,13 @@ export const getDefaultQuery = ({ filters, sort: { field: '@timestamp', direction: 'desc' }, pageIndex: 0, - pageSize: 10, }); -const MAX_ITEMS = 500; - export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const [pageSize, setPageSize] = useLocalStorage( - LOCAL_STORAGE_PAGE_SIZE_LATEST_FINDINGS_KEY, - urlQuery.pageSize - ); + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY); + /** * Page URL query to ES query */ @@ -67,26 +65,24 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { * Page ES query result */ const findingsGroupByNone = useLatestFindings({ - ...getPaginationQuery({ - pageIndex: urlQuery.pageIndex, - pageSize: pageSize || urlQuery.pageSize, - }), query: baseEsQuery.query, sort: urlQuery.sort, enabled: !baseEsQuery.error, }); + const slicedPage = usePageSlice(findingsGroupByNone.data?.page, urlQuery.pageIndex, pageSize); + const error = findingsGroupByNone.error || baseEsQuery.error; const { isLastLimitedPage, limitedTotalItemCount } = useMemo( () => getLimitProperties( findingsGroupByNone.data?.total || 0, - MAX_ITEMS, - urlQuery.pageSize, + MAX_FINDINGS_TO_LOAD, + pageSize, urlQuery.pageIndex ), - [findingsGroupByNone.data?.total, urlQuery.pageIndex, urlQuery.pageSize] + [findingsGroupByNone.data?.total, urlQuery.pageIndex, pageSize] ); const handleDistributionClick = (evaluation: Evaluation) => { @@ -134,8 +130,8 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { failed: findingsGroupByNone.data.count.failed, ...getFindingsPageSizeInfo({ pageIndex: urlQuery.pageIndex, - pageSize: urlQuery.pageSize, - currentPageSize: findingsGroupByNone.data.page.length, + pageSize, + currentPageSize: slicedPage.length, }), }} /> @@ -143,9 +139,9 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { { setUrlQuery({ sort, pageIndex: page.index, - pageSize: page.size, }); }} onAddFilter={(field, value, negate) => @@ -182,7 +177,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { id="xpack.csp.findings.latestFindings.bottomBarLabel" defaultMessage="These are the first {maxItems} findings matching your search, refine your search to see others." values={{ - maxItems: MAX_ITEMS, + maxItems: MAX_FINDINGS_TO_LOAD, }} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts index 24028102536a6..b938c202014d8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useContext } from 'react'; import { useQuery } from '@tanstack/react-query'; import { number } from 'io-ts'; import { lastValueFrom } from 'rxjs'; @@ -14,24 +13,21 @@ import type { Pagination } from '@elastic/eui'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { i18n } from '@kbn/i18n'; import { CspFinding } from '../../../../common/schemas/csp_finding'; -import { FindingsEsPitContext } from '../es_pit/findings_es_pit_context'; import { extractErrorMessage } from '../../../../common/utils/helpers'; import type { Sort } from '../types'; import { useKibana } from '../../../common/hooks/use_kibana'; import type { FindingsBaseEsQuery } from '../types'; -import { FINDINGS_REFETCH_INTERVAL_MS } from '../constants'; import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils'; +import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; +import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; interface UseFindingsOptions extends FindingsBaseEsQuery { - from: NonNullable['from']>; - size: NonNullable['size']>; sort: Sort; enabled: boolean; } export interface FindingsGroupByNoneQuery { pageIndex: Pagination['pageIndex']; - pageSize: Pagination['pageSize']; sort: Sort; } @@ -57,21 +53,14 @@ export const showErrorToast = ( else toasts.addDanger(extractErrorMessage(error, SEARCH_FAILED_TEXT)); }; -export const getFindingsQuery = ({ - query, - size, - from, - sort, - pitId, -}: UseFindingsOptions & { pitId: string }) => ({ +export const getFindingsQuery = ({ query, sort }: UseFindingsOptions) => ({ + index: CSP_LATEST_FINDINGS_DATA_VIEW, body: { query, sort: [{ [sort.field]: sort.direction }], - size, - from, + size: MAX_FINDINGS_TO_LOAD, aggs: getFindingsCountAggQuery(), }, - pit: { id: pitId }, ignore_unavailable: false, }); @@ -80,22 +69,17 @@ export const useLatestFindings = (options: UseFindingsOptions) => { data, notifications: { toasts }, } = useKibana().services; - const { pitIdRef, setPitId } = useContext(FindingsEsPitContext); - const params = { ...options, pitId: pitIdRef.current }; - return useQuery( - ['csp_findings', { params }], + ['csp_findings', { params: options }], async () => { const { - rawResponse: { hits, aggregations, pit_id: newPitId }, + rawResponse: { hits, aggregations }, } = await lastValueFrom( data.search.search({ - params: getFindingsQuery(params), + params: getFindingsQuery(options), }) ); - if (!aggregations) throw new Error('expected aggregations to be an defined'); - if (!Array.isArray(aggregations.count.buckets)) throw new Error('expected buckets to be an array'); @@ -103,19 +87,12 @@ export const useLatestFindings = (options: UseFindingsOptions) => { page: hits.hits.map((hit) => hit._source!), total: number.is(hits.total) ? hits.total : 0, count: getAggregationCount(aggregations.count.buckets), - newPitId: newPitId!, }; }, { enabled: options.enabled, keepPreviousData: true, onError: (err: Error) => showErrorToast(toasts, err), - onSuccess: ({ newPitId }) => { - setPitId(newPitId); - }, - // Refetching on an interval to ensure the PIT window stays open - refetchInterval: FINDINGS_REFETCH_INTERVAL_MS, - refetchIntervalInBackground: true, } ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx index 74bddc75109c4..1a373fc4174e0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx @@ -8,20 +8,20 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import type { Evaluation } from '../../../../common/types'; import { CloudPosturePageTitle } from '../../../components/cloud_posture_page_title'; import { FindingsSearchBar } from '../layout/findings_search_bar'; import * as TEST_SUBJECTS from '../test_subjects'; import { useUrlQuery } from '../../../common/hooks/use_url_query'; +import { usePageSlice } from '../../../common/hooks/use_page_slice'; +import { usePageSize } from '../../../common/hooks/use_page_size'; import type { FindingsBaseProps, FindingsBaseURLQuery } from '../types'; import { FindingsByResourceQuery, useFindingsByResource } from './use_findings_by_resource'; import { FindingsByResourceTable } from './findings_by_resource_table'; import { getFindingsPageSizeInfo, getFilters, - getPaginationQuery, getPaginationTableParams, useBaseEsQuery, usePersistedQuery, @@ -32,7 +32,7 @@ import { findingsNavigation } from '../../../common/navigation/constants'; import { ResourceFindings } from './resource_findings/resource_findings_container'; import { ErrorCallout } from '../layout/error_callout'; import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; -import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_BY_RESOURCE_KEY } from '../../../../common/constants'; +import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../../common/constants'; const getDefaultQuery = ({ query, @@ -41,7 +41,6 @@ const getDefaultQuery = ({ query, filters, pageIndex: 0, - pageSize: 10, sortDirection: 'desc', }); @@ -70,10 +69,7 @@ export const FindingsByResourceContainer = ({ dataView }: FindingsBaseProps) => const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const [pageSize, setPageSize] = useLocalStorage( - LOCAL_STORAGE_PAGE_SIZE_FINDINGS_BY_RESOURCE_KEY, - urlQuery.pageSize - ); + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY); /** * Page URL query to ES query @@ -88,10 +84,6 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { * Page ES query result */ const findingsGroupByResource = useFindingsByResource({ - ...getPaginationQuery({ - pageIndex: urlQuery.pageIndex, - pageSize: pageSize || urlQuery.pageSize, - }), sortDirection: urlQuery.sortDirection, query: baseEsQuery.query, enabled: !baseEsQuery.error, @@ -99,6 +91,8 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { const error = findingsGroupByResource.error || baseEsQuery.error; + const slicedPage = usePageSlice(findingsGroupByResource.data?.page, urlQuery.pageIndex, pageSize); + const handleDistributionClick = (evaluation: Evaluation) => { setUrlQuery({ pageIndex: 0, @@ -155,8 +149,8 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { failed: findingsGroupByResource.data.count.failed, ...getFindingsPageSizeInfo({ pageIndex: urlQuery.pageIndex, - pageSize: urlQuery.pageSize, - currentPageSize: findingsGroupByResource.data.page.length, + pageSize, + currentPageSize: slicedPage.length, }), }} /> @@ -164,9 +158,9 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { { setUrlQuery({ sortDirection: sort?.direction, pageIndex: page.index, - pageSize: page.size, }); }} sorting={{ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx index e61415b7d0af1..f18738a0736fa 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx @@ -15,7 +15,6 @@ import { Link, useParams } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; import { generatePath } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; import { CspInlineDescriptionList } from '../../../../components/csp_inline_description_list'; import type { Evaluation } from '../../../../../common/types'; import { CspFinding } from '../../../../../common/schemas/csp_finding'; @@ -25,11 +24,12 @@ import { PageTitle, PageTitleText } from '../../layout/findings_layout'; import { findingsNavigation } from '../../../../common/navigation/constants'; import { ResourceFindingsQuery, useResourceFindings } from './use_resource_findings'; import { useUrlQuery } from '../../../../common/hooks/use_url_query'; +import { usePageSlice } from '../../../../common/hooks/use_page_slice'; +import { usePageSize } from '../../../../common/hooks/use_page_size'; import type { FindingsBaseURLQuery, FindingsBaseProps } from '../../types'; import { getFindingsPageSizeInfo, getFilters, - getPaginationQuery, getPaginationTableParams, useBaseEsQuery, usePersistedQuery, @@ -38,7 +38,7 @@ import { ResourceFindingsTable } from './resource_findings_table'; import { FindingsSearchBar } from '../../layout/findings_search_bar'; import { ErrorCallout } from '../../layout/error_callout'; import { FindingsDistributionBar } from '../../layout/findings_distribution_bar'; -import { LOCAL_STORAGE_PAGE_SIZE_RESOURCE_FINDINGS_KEY } from '../../../../../common/constants'; +import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../../../common/constants'; const getDefaultQuery = ({ query, @@ -48,7 +48,6 @@ const getDefaultQuery = ({ filters, sort: { field: 'result.evaluation' as keyof CspFinding, direction: 'asc' }, pageIndex: 0, - pageSize: 10, }); const BackToResourcesButton = () => ( @@ -92,10 +91,7 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { const params = useParams<{ resourceId: string }>(); const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const [pageSize, setPageSize] = useLocalStorage( - LOCAL_STORAGE_PAGE_SIZE_RESOURCE_FINDINGS_KEY, - urlQuery.pageSize - ); + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY); /** * Page URL query to ES query @@ -110,10 +106,6 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { * Page ES query result */ const resourceFindings = useResourceFindings({ - ...getPaginationQuery({ - pageSize: pageSize || urlQuery.pageSize, - pageIndex: urlQuery.pageIndex, - }), sort: urlQuery.sort, query: baseEsQuery.query, resourceId: params.resourceId, @@ -122,6 +114,8 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { const error = resourceFindings.error || baseEsQuery.error; + const slicedPage = usePageSlice(resourceFindings.data?.page, urlQuery.pageIndex, pageSize); + const handleDistributionClick = (evaluation: Evaluation) => { setUrlQuery({ pageIndex: 0, @@ -190,8 +184,8 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { failed: resourceFindings.data.count.failed, ...getFindingsPageSizeInfo({ pageIndex: urlQuery.pageIndex, - pageSize: urlQuery.pageSize, - currentPageSize: resourceFindings.data.page.length, + pageSize, + currentPageSize: slicedPage.length, }), }} /> @@ -199,9 +193,9 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { { }} setTableOptions={({ page, sort }) => { setPageSize(page.size); - setUrlQuery({ pageIndex: page.index, pageSize: page.size, sort }); + setUrlQuery({ pageIndex: page.index, sort }); }} onAddFilter={(field, value, negate) => setUrlQuery({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts index e65f2ea4bfd23..95645d20b16a0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts @@ -9,27 +9,23 @@ import { lastValueFrom } from 'rxjs'; import { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Pagination } from '@elastic/eui'; -import { useContext } from 'react'; import { number } from 'io-ts'; import { CspFinding } from '../../../../../common/schemas/csp_finding'; import { getAggregationCount, getFindingsCountAggQuery } from '../../utils/utils'; -import { FindingsEsPitContext } from '../../es_pit/findings_es_pit_context'; -import { FINDINGS_REFETCH_INTERVAL_MS } from '../../constants'; import { useKibana } from '../../../../common/hooks/use_kibana'; import { showErrorToast } from '../../latest_findings/use_latest_findings'; import type { FindingsBaseEsQuery, Sort } from '../../types'; +import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../../common/constants'; +import { MAX_FINDINGS_TO_LOAD } from '../../../../common/constants'; interface UseResourceFindingsOptions extends FindingsBaseEsQuery { resourceId: string; - from: NonNullable['from']>; - size: NonNullable['size']>; sort: Sort; enabled: boolean; } export interface ResourceFindingsQuery { pageIndex: Pagination['pageIndex']; - pageSize: Pagination['pageSize']; sort: Sort; } @@ -46,14 +42,11 @@ export type ResourceFindingsResponseAggs = Record< const getResourceFindingsQuery = ({ query, resourceId, - from, - size, - pitId, sort, -}: UseResourceFindingsOptions & { pitId: string }): estypes.SearchRequest => ({ +}: UseResourceFindingsOptions): estypes.SearchRequest => ({ + index: CSP_LATEST_FINDINGS_DATA_VIEW, body: { - from, - size, + size: MAX_FINDINGS_TO_LOAD, query: { ...query, bool: { @@ -62,7 +55,6 @@ const getResourceFindingsQuery = ({ }, }, sort: [{ [sort.field]: sort.direction }], - pit: { id: pitId }, aggs: { ...getFindingsCountAggQuery(), clusterId: { @@ -85,8 +77,7 @@ export const useResourceFindings = (options: UseResourceFindingsOptions) => { notifications: { toasts }, } = useKibana().services; - const { pitIdRef, setPitId } = useContext(FindingsEsPitContext); - const params = { ...options, pitId: pitIdRef.current }; + const params = { ...options }; return useQuery( ['csp_resource_findings', { params }], @@ -99,9 +90,7 @@ export const useResourceFindings = (options: UseResourceFindingsOptions) => { { enabled: options.enabled, keepPreviousData: true, - select: ({ - rawResponse: { hits, pit_id: newPitId, aggregations }, - }: ResourceFindingsResponse) => { + select: ({ rawResponse: { hits, aggregations } }: ResourceFindingsResponse) => { if (!aggregations) throw new Error('expected aggregations to exists'); assertNonEmptyArray(aggregations.count.buckets); @@ -116,16 +105,9 @@ export const useResourceFindings = (options: UseResourceFindingsOptions) => { clusterId: getFirstBucketKey(aggregations.clusterId.buckets), resourceSubType: getFirstBucketKey(aggregations.resourceSubType.buckets), resourceName: getFirstBucketKey(aggregations.resourceName.buckets), - newPitId: newPitId!, }; }, onError: (err: Error) => showErrorToast(toasts, err), - onSuccess: ({ newPitId }) => { - setPitId(newPitId); - }, - // Refetching on an interval to ensure the PIT window stays open - refetchInterval: FINDINGS_REFETCH_INTERVAL_MS, - refetchIntervalInBackground: true, } ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts index 000100a889139..139013d88cec4 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts @@ -4,22 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useContext } from 'react'; import { useQuery } from '@tanstack/react-query'; import { lastValueFrom } from 'rxjs'; import { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Pagination } from '@elastic/eui'; -import { FindingsEsPitContext } from '../es_pit/findings_es_pit_context'; -import { FINDINGS_REFETCH_INTERVAL_MS } from '../constants'; import { useKibana } from '../../../common/hooks/use_kibana'; import { showErrorToast } from '../latest_findings/use_latest_findings'; import type { FindingsBaseEsQuery, Sort } from '../types'; import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils'; +import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; +import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; interface UseFindingsByResourceOptions extends FindingsBaseEsQuery { - from: NonNullable['from']>; - size: NonNullable['size']>; enabled: boolean; sortDirection: Sort['direction']; } @@ -27,13 +24,8 @@ interface UseFindingsByResourceOptions extends FindingsBaseEsQuery { // Maximum number of grouped findings, default limit in elasticsearch is set to 65,536 (ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-settings.html#search-settings-max-buckets) const MAX_BUCKETS = 60 * 1000; -interface UseResourceFindingsQueryOptions extends Omit { - pitId: string; -} - export interface FindingsByResourceQuery { pageIndex: Pagination['pageIndex']; - pageSize: Pagination['pageSize']; sortDirection: Sort['direction']; } @@ -73,11 +65,9 @@ interface FindingsAggBucket extends estypes.AggregationsStringRareTermsBucketKey export const getFindingsByResourceAggQuery = ({ query, - from, - size, - pitId, sortDirection, -}: UseResourceFindingsQueryOptions): estypes.SearchRequest => ({ +}: UseFindingsByResourceOptions): estypes.SearchRequest => ({ + index: CSP_LATEST_FINDINGS_DATA_VIEW, body: { query, size: 0, @@ -107,8 +97,7 @@ export const getFindingsByResourceAggQuery = ({ }, sort_failed_findings: { bucket_sort: { - from, - size, + size: MAX_FINDINGS_TO_LOAD, sort: [ { 'failed_findings>_count': { order: sortDirection }, @@ -121,7 +110,6 @@ export const getFindingsByResourceAggQuery = ({ }, }, }, - pit: { id: pitId }, }, ignore_unavailable: false, }); @@ -132,14 +120,13 @@ export const useFindingsByResource = (options: UseFindingsByResourceOptions) => notifications: { toasts }, } = useKibana().services; - const { pitIdRef, setPitId } = useContext(FindingsEsPitContext); - const params = { ...options, pitId: pitIdRef.current }; + const params = { ...options }; return useQuery( ['csp_findings_resource', { params }], async () => { const { - rawResponse: { aggregations, pit_id: newPitId }, + rawResponse: { aggregations }, } = await lastValueFrom( data.search.search({ params: getFindingsByResourceAggQuery(params), @@ -158,19 +145,12 @@ export const useFindingsByResource = (options: UseFindingsByResourceOptions) => page: aggregations.resources.buckets.map(createFindingsByResource), total: aggregations.resource_total.value, count: getAggregationCount(aggregations.count.buckets), - newPitId: newPitId!, }; }, { enabled: options.enabled, keepPreviousData: true, onError: (err: Error) => showErrorToast(toasts, err), - onSuccess: ({ newPitId }) => { - setPitId(newPitId); - }, - // Refetching on an interval to ensure the PIT window stays open - refetchInterval: FINDINGS_REFETCH_INTERVAL_MS, - refetchIntervalInBackground: true, } ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index 98b1655c7195d..534d3d2f34237 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -7,7 +7,6 @@ import React, { useState, useMemo } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { useParams } from 'react-router-dom'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; import { extractErrorMessage, createCspRuleSearchFilterByPackagePolicy, @@ -23,7 +22,8 @@ import { } from './use_csp_rules'; import * as TEST_SUBJECTS from './test_subjects'; import { RuleFlyout } from './rules_flyout'; -import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../../common/constants'; +import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../common/constants'; +import { usePageSize } from '../../common/hooks/use_page_size'; interface RulesPageData { rules_page: RuleSavedObject[]; @@ -70,7 +70,7 @@ export type PageUrlParams = Record<'policyId' | 'packagePolicyId', string>; export const RulesContainer = () => { const params = useParams(); const [selectedRuleId, setSelectedRuleId] = useState(null); - const [pageSize, setPageSize] = useLocalStorage(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY, 10); + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); const [rulesQuery, setRulesQuery] = useState({ filter: createCspRuleSearchFilterByPackagePolicy({ packagePolicyId: params.packagePolicyId, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/es_pit/es_pit.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/es_pit/es_pit.test.ts deleted file mode 100644 index 8447e393f2f51..0000000000000 --- a/x-pack/plugins/cloud_security_posture/server/routes/es_pit/es_pit.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { Chance } from 'chance'; -import { - elasticsearchClientMock, - ElasticsearchClientMock, -} from '@kbn/core-elasticsearch-client-server-mocks'; -import type { ElasticsearchClient } from '@kbn/core/server'; -import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; -import { DEFAULT_PIT_KEEP_ALIVE, defineEsPitRoute, esPitInputSchema } from './es_pit'; -import { createCspRequestHandlerContextMock } from '../../mocks'; - -describe('ES Point in time API endpoint', () => { - const chance = new Chance(); - let mockEsClient: jest.Mocked; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('validate the API route path', () => { - const router = httpServiceMock.createRouter(); - - defineEsPitRoute(router); - - const [config] = router.post.mock.calls[0]; - expect(config.path).toEqual('/internal/cloud_security_posture/es_pit'); - }); - - it('should accept to a user with fleet.all privilege', async () => { - const router = httpServiceMock.createRouter(); - - defineEsPitRoute(router); - - const mockContext = createCspRequestHandlerContextMock(); - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - const [context, req, res] = [mockContext, mockRequest, mockResponse]; - - const [_, handler] = router.post.mock.calls[0]; - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledTimes(0); - }); - - it('should reject to a user without fleet.all privilege', async () => { - const router = httpServiceMock.createRouter(); - - defineEsPitRoute(router); - - const mockContext = createCspRequestHandlerContextMock(); - mockContext.fleet.authz.fleet.all = false; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - const [context, req, res] = [mockContext, mockRequest, mockResponse]; - - const [_, handler] = router.post.mock.calls[0]; - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledTimes(1); - }); - - it('should return the newly created PIT ID from ES', async () => { - const router = httpServiceMock.createRouter(); - - defineEsPitRoute(router); - - const pitId = chance.string(); - mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; - mockEsClient.openPointInTime.mockImplementation(() => Promise.resolve({ id: pitId })); - - const mockContext = createCspRequestHandlerContextMock(); - mockContext.core.elasticsearch.client.asCurrentUser = mockEsClient as ElasticsearchClientMock; - - const indexName = chance.string(); - const keepAlive = chance.string(); - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest({ - query: { index_name: indexName, keep_alive: keepAlive }, - }); - - const [context, req, res] = [mockContext, mockRequest, mockResponse]; - const [_, handler] = router.post.mock.calls[0]; - await handler(context, req, res); - - expect(mockEsClient.openPointInTime).toHaveBeenCalledTimes(1); - expect(mockEsClient.openPointInTime).toHaveBeenLastCalledWith({ - index: indexName, - keep_alive: keepAlive, - }); - - expect(res.ok).toHaveBeenCalledTimes(1); - expect(res.ok).toHaveBeenLastCalledWith({ body: pitId }); - }); - - describe('test input schema', () => { - it('passes keep alive and index name parameters', () => { - const indexName = chance.string(); - const keepAlive = chance.string(); - const validatedQuery = esPitInputSchema.validate({ - index_name: indexName, - keep_alive: keepAlive, - }); - - expect(validatedQuery).toMatchObject({ - index_name: indexName, - keep_alive: keepAlive, - }); - }); - - it('populates default keep alive parameter value', () => { - const indexName = chance.string(); - const validatedQuery = esPitInputSchema.validate({ index_name: indexName }); - - expect(validatedQuery).toMatchObject({ - index_name: indexName, - keep_alive: DEFAULT_PIT_KEEP_ALIVE, - }); - }); - - it('throws when index name parameter is not passed', () => { - expect(() => { - esPitInputSchema.validate({}); - }).toThrow(); - }); - - it('throws when index name parameter is not a string', () => { - const indexName = chance.integer(); - expect(() => { - esPitInputSchema.validate({ index_name: indexName }); - }).toThrow(); - }); - - it('throws when keep alive parameter is not a string', () => { - const indexName = chance.string(); - const keepAlive = chance.integer(); - expect(() => { - esPitInputSchema.validate({ index_name: indexName, keep_alive: keepAlive }); - }).toThrow(); - }); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/es_pit/es_pit.ts b/x-pack/plugins/cloud_security_posture/server/routes/es_pit/es_pit.ts deleted file mode 100644 index beba811429aa5..0000000000000 --- a/x-pack/plugins/cloud_security_posture/server/routes/es_pit/es_pit.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { transformError } from '@kbn/securitysolution-es-utils'; -import { ES_PIT_ROUTE_PATH } from '../../../common/constants'; -import type { CspRouter } from '../../types'; - -export const DEFAULT_PIT_KEEP_ALIVE = '1m'; - -export const esPitInputSchema = schema.object({ - index_name: schema.string(), - keep_alive: schema.string({ defaultValue: DEFAULT_PIT_KEEP_ALIVE }), -}); - -export const defineEsPitRoute = (router: CspRouter): void => - router.post( - { - path: ES_PIT_ROUTE_PATH, - validate: { query: esPitInputSchema }, - options: { - tags: ['access:cloud-security-posture-read'], - }, - }, - async (context, request, response) => { - const cspContext = await context.csp; - - if (!(await context.fleet).authz.fleet.all) { - return response.forbidden(); - } - - try { - const esClient = cspContext.esClient.asCurrentUser; - const { id } = await esClient.openPointInTime({ - index: request.query.index_name, - keep_alive: request.query.keep_alive, - }); - - return response.ok({ body: id }); - } catch (err) { - const error = transformError(err); - cspContext.logger.error(`Failed to open Elasticsearch point in time: ${error}`); - return response.customError({ - body: { message: error.message }, - statusCode: error.statusCode, - }); - } - } - ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts index 4c2901edde56b..ed31e5fd6bf7a 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts @@ -16,7 +16,6 @@ import { PLUGIN_ID } from '../../common'; import { defineGetComplianceDashboardRoute } from './compliance_dashboard/compliance_dashboard'; import { defineGetBenchmarksRoute } from './benchmarks/benchmarks'; import { defineGetCspStatusRoute } from './status/status'; -import { defineEsPitRoute } from './es_pit/es_pit'; /** * 1. Registers routes @@ -33,7 +32,6 @@ export function setupRoutes({ defineGetComplianceDashboardRoute(router); defineGetBenchmarksRoute(router); defineGetCspStatusRoute(router); - defineEsPitRoute(router); core.http.registerRouteHandlerContext( PLUGIN_ID, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts index f828d34bdde12..ce941613e3587 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/types.ts @@ -161,6 +161,12 @@ export interface DomainConfigFromServer { sitemap_urls: string[]; } +export interface CrawlScheduleFromServer { + frequency: number; + unit: CrawlUnits; + use_connector_schedule: boolean; +} + // Client export interface CrawlerDomain { @@ -252,6 +258,7 @@ export type CrawlEvent = CrawlRequest & { export interface CrawlSchedule { frequency: number; unit: CrawlUnits; + useConnectorSchedule: boolean; } export interface DomainConfig { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts index ab47d8a575c5b..7886d349044c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/utils.ts @@ -29,6 +29,8 @@ import { BasicCrawlerAuth, CrawlerAuth, RawCrawlerAuth, + CrawlScheduleFromServer, + CrawlSchedule, } from './types'; export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): CrawlerDomain { @@ -241,6 +243,16 @@ export const crawlerDomainsWithMetaServerToClient = ({ meta, }); +export const crawlScheduleServerToClient = ({ + frequency, + unit, + use_connector_schedule: useConnectorSchedule, +}: CrawlScheduleFromServer): CrawlSchedule => ({ + frequency, + unit, + useConnectorSchedule, +}); + export function isBasicCrawlerAuth(auth: CrawlerAuth): auth is BasicCrawlerAuth { return auth !== null && (auth as BasicCrawlerAuth).type === 'basic'; } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/constants.ts index c7a6b2d576a30..c39d26314671c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/constants.ts @@ -8,6 +8,8 @@ import { EuiSelectOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { languageToText } from '../../utils/language_to_text'; + export const NEW_INDEX_TEMPLATE_TYPES: { [key: string]: string } = { api: i18n.translate('xpack.enterpriseSearch.content.newIndex.types.api', { defaultMessage: 'API endpoint', @@ -45,142 +47,67 @@ export const UNIVERSAL_LANGUAGE_VALUE = ''; export const SUPPORTED_LANGUAGES: EuiSelectOption[] = [ { + text: languageToText(UNIVERSAL_LANGUAGE_VALUE), value: UNIVERSAL_LANGUAGE_VALUE, - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.universalDropDownOptionLabel', - { - defaultMessage: 'Universal', - } - ), }, { - text: '—', disabled: true, + text: '—', }, { + text: languageToText('zh'), value: 'zh', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.chineseDropDownOptionLabel', - { - defaultMessage: 'Chinese', - } - ), }, { + text: languageToText('da'), value: 'da', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.danishDropDownOptionLabel', - { - defaultMessage: 'Danish', - } - ), }, { + text: languageToText('nl'), value: 'nl', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.dutchDropDownOptionLabel', - { - defaultMessage: 'Dutch', - } - ), }, { + text: languageToText('en'), value: 'en', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.englishDropDownOptionLabel', - { - defaultMessage: 'English', - } - ), }, { + text: languageToText('fr'), value: 'fr', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.frenchDropDownOptionLabel', - { - defaultMessage: 'French', - } - ), }, { + text: languageToText('de'), value: 'de', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.germanDropDownOptionLabel', - { - defaultMessage: 'German', - } - ), }, { + text: languageToText('it'), value: 'it', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.italianDropDownOptionLabel', - { - defaultMessage: 'Italian', - } - ), }, { + text: languageToText('ja'), value: 'ja', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.japaneseDropDownOptionLabel', - { - defaultMessage: 'Japanese', - } - ), }, { + text: languageToText('ko'), value: 'ko', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.koreanDropDownOptionLabel', - { - defaultMessage: 'Korean', - } - ), }, { + text: languageToText('pt'), value: 'pt', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseDropDownOptionLabel', - { - defaultMessage: 'Portuguese', - } - ), }, { + text: languageToText('pt-br'), value: 'pt-br', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseBrazilDropDownOptionLabel', - { - defaultMessage: 'Portuguese (Brazil)', - } - ), }, { + text: languageToText('ru'), value: 'ru', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.russianDropDownOptionLabel', - { - defaultMessage: 'Russian', - } - ), }, { + text: languageToText('es'), value: 'es', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.spanishDropDownOptionLabel', - { - defaultMessage: 'Spanish', - } - ), }, { + text: languageToText('th'), value: 'th', - text: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.supportedLanguages.thaiDropDownOptionLabel', - { - defaultMessage: 'Thai', - } - ), }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector_total_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector_total_stats.tsx index 2b5ef4cb68df7..fd70516a572d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector_total_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector_total_stats.tsx @@ -21,6 +21,8 @@ import { i18n } from '@kbn/i18n'; import { isConnectorIndex } from '../../utils/indices'; +import { languageToText } from '../../utils/language_to_text'; + import { ConnectorOverviewPanels } from './connector/connector_overview_panels'; import { NATIVE_CONNECTORS } from './connector/constants'; import { NameAndDescriptionStats } from './name_and_description_stats'; @@ -71,11 +73,7 @@ export const ConnectorTotalStats: React.FC = () => { } ), isLoading: hideStats, - title: - indexData.connector.language ?? - i18n.translate('xpack.enterpriseSearch.content.searchIndex.totalStats.noneLabel', { - defaultMessage: 'None', - }), + title: languageToText(indexData.connector.language ?? ''), }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler.test.tsx deleted file mode 100644 index a81ae20408aa0..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; -import '../../../../../__mocks__/shallow_useeffect.mock'; - -import React from 'react'; - -import { shallow, ShallowWrapper } from 'enzyme'; - -import { EuiButton, EuiFieldNumber, EuiForm, EuiSelect, EuiSwitch } from '@elastic/eui'; - -import { CrawlUnits } from '../../../../api/crawler/types'; - -import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; - -const MOCK_ACTIONS = { - // AutomaticCrawlSchedulerLogic - setCrawlFrequency: jest.fn(), - setCrawlUnit: jest.fn(), - saveChanges: jest.fn(), - toggleCrawlAutomatically: jest.fn(), -}; - -const MOCK_VALUES = { - crawlAutomatically: false, - crawlFrequency: 7, - crawlUnit: CrawlUnits.days, - isSubmitting: false, -}; - -describe('AutomaticCrawlScheduler', () => { - let wrapper: ShallowWrapper; - - beforeEach(() => { - setMockActions(MOCK_ACTIONS); - setMockValues(MOCK_VALUES); - - wrapper = shallow(); - }); - - it('renders', () => { - expect(wrapper.find(EuiForm)).toHaveLength(1); - expect(wrapper.find(EuiFieldNumber)).toHaveLength(1); - expect(wrapper.find(EuiSelect)).toHaveLength(1); - }); - - it('saves changes on form submit', () => { - const preventDefault = jest.fn(); - wrapper.find(EuiForm).simulate('submit', { preventDefault }); - - expect(preventDefault).toHaveBeenCalled(); - expect(MOCK_ACTIONS.saveChanges).toHaveBeenCalled(); - }); - - it('contains a switch that toggles automatic crawling', () => { - wrapper.find(EuiSwitch).simulate('change'); - - expect(MOCK_ACTIONS.toggleCrawlAutomatically).toHaveBeenCalled(); - }); - - it('contains a number field that updates the crawl frequency', () => { - wrapper.find(EuiFieldNumber).simulate('change', { target: { value: '10' } }); - - expect(MOCK_ACTIONS.setCrawlFrequency).toHaveBeenCalledWith(10); - }); - - it('contains a select field that updates the crawl unit', () => { - wrapper.find(EuiSelect).simulate('change', { target: { value: CrawlUnits.weeks } }); - - expect(MOCK_ACTIONS.setCrawlUnit).toHaveBeenCalledWith(CrawlUnits.weeks); - }); - - it('contains a submit button', () => { - expect(wrapper.find(EuiButton).prop('type')).toEqual('submit'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler.tsx index 28c8b8ff10000..960e432762722 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler.tsx @@ -10,175 +10,220 @@ import React from 'react'; import { useActions, useValues } from 'kea'; import { - EuiButton, + EuiCheckableCard, EuiFieldNumber, EuiFlexGroup, EuiFlexItem, - EuiForm, EuiFormRow, + EuiHorizontalRule, EuiLink, EuiSelect, EuiSpacer, + EuiSplitPanel, EuiSwitch, EuiText, - htmlIdGenerator, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - +import { CrawlerIndex } from '../../../../../../../common/types/indices'; import { HOURS_UNIT_LABEL, DAYS_UNIT_LABEL, WEEKS_UNIT_LABEL, MONTHS_UNIT_LABEL, - SAVE_BUTTON_LABEL, } from '../../../../../shared/constants'; -import { DataPanel } from '../../../../../shared/data_panel/data_panel'; - +import { EnterpriseSearchCronEditor } from '../../../../../shared/cron_editor/enterprise_search_cron_editor'; import { docLinks } from '../../../../../shared/doc_links/doc_links'; +import { UpdateConnectorSchedulingApiLogic } from '../../../../api/connector/update_connector_scheduling_api_logic'; import { CrawlUnits } from '../../../../api/crawler/types'; +import { isCrawlerIndex } from '../../../../utils/indices'; +import { IndexViewLogic } from '../../index_view_logic'; import { AutomaticCrawlSchedulerLogic } from './automatic_crawl_scheduler_logic'; export const AutomaticCrawlScheduler: React.FC = () => { - const { setCrawlFrequency, setCrawlUnit, saveChanges, toggleCrawlAutomatically } = useActions( - AutomaticCrawlSchedulerLogic - ); + const { index } = useValues(IndexViewLogic); + const { makeRequest } = useActions(UpdateConnectorSchedulingApiLogic); - const { crawlAutomatically, crawlFrequency, crawlUnit, isSubmitting } = useValues( + const scheduling = (index as CrawlerIndex)?.connector?.scheduling; + + const { setCrawlFrequency, setCrawlUnit, setUseConnectorSchedule, toggleCrawlAutomatically } = + useActions(AutomaticCrawlSchedulerLogic); + + const { crawlAutomatically, crawlFrequency, crawlUnit, useConnectorSchedule } = useValues( AutomaticCrawlSchedulerLogic ); - const formId = htmlIdGenerator('AutomaticCrawlScheduler')(); + if (!isCrawlerIndex(index)) { + return <>; + } return ( <> - - {i18n.translate('xpack.enterpriseSearch.automaticCrawlSchedule.title', { - defaultMessage: 'Automated Crawl Scheduling', - })} - - } - titleSize="s" - subtitle={ - - {i18n.translate( - 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.readMoreLink', - { - defaultMessage: 'Read more.', - } - )} - - ), - }} - /> - } - iconType="calendar" - > - { - event.preventDefault(); - saveChanges(); - }} - component="form" - id={formId} - > + +

+ {i18n.translate('xpack.enterpriseSearch.automaticCrawlSchedule.title', { + defaultMessage: 'Crawl frequency', + })} +

+
+ + + - {i18n.translate( - 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlAutomaticallySwitchLabel', - { - defaultMessage: 'Crawl automatically', - } - )} - - } + label={i18n.translate( + 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlAutomaticallySwitchLabel', + { + defaultMessage: 'Enable recurring crawls with the following schedule', + } + )} onChange={toggleCrawlAutomatically} compressed /> - - - - - {i18n.translate( - 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlUnitsPrefix', - { - defaultMessage: 'Every', - } - )} - - - - setCrawlFrequency(parseInt(e.target.value, 10))} + + + + + + +
+ {i18n.translate( + 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.cronSchedulingTitle', + { + defaultMessage: 'Specific time scheduling', + } + )} +
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.cronSchedulingDescription', + { + defaultMessage: 'Define the frequency and time for scheduled crawls', + } + )} + + + + } + checked={crawlAutomatically && useConnectorSchedule} + disabled={!crawlAutomatically} + onChange={() => setUseConnectorSchedule(true)} + > + + makeRequest({ + connectorId: index.connector.id, + scheduling: { ...newScheduling }, + }) + } /> -
- - setCrawlUnit(e.target.value as CrawlUnits)} - /> - - -
- + + + + + +
+ {i18n.translate( + 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.intervalSchedulingTitle', + { + defaultMessage: 'Interval scheduling', + } + )} +
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.intervalSchedulingDescription', + { + defaultMessage: 'Define the frequency and time for scheduled crawls', + } + )} + + + + } + checked={crawlAutomatically && !useConnectorSchedule} + disabled={!crawlAutomatically} + onChange={() => setUseConnectorSchedule(false)} + > + + + + setCrawlFrequency(parseInt(e.target.value, 10))} + prepend={'Every'} + /> + + + setCrawlUnit(e.target.value as CrawlUnits)} + /> + + + +
+
+ {i18n.translate( @@ -188,21 +233,18 @@ export const AutomaticCrawlScheduler: React.FC = () => { 'The crawl schedule will perform a full crawl on every domain on this index.', } )} + + + {i18n.translate( + 'xpack.enterpriseSearch.crawler.automaticCrawlSchedule.readMoreLink', + { + defaultMessage: 'Learn more about scheduling', + } + )} + - - - - {SAVE_BUTTON_LABEL} - - -
-
+ + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.test.ts index 2327c1c394cee..ac324eac83c37 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.test.ts @@ -21,7 +21,7 @@ import { AutomaticCrawlSchedulerLogic } from './automatic_crawl_scheduler_logic' describe('AutomaticCrawlSchedulerLogic', () => { const { mount } = new LogicMounter(AutomaticCrawlSchedulerLogic); const { http } = mockHttpValues; - const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { flashAPIErrors } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); @@ -35,6 +35,7 @@ describe('AutomaticCrawlSchedulerLogic', () => { crawlFrequency: 24, crawlUnit: CrawlUnits.hours, isSubmitting: false, + useConnectorSchedule: false, }); }); @@ -102,6 +103,7 @@ describe('AutomaticCrawlSchedulerLogic', () => { AutomaticCrawlSchedulerLogic.actions.setCrawlSchedule({ frequency: 3, unit: CrawlUnits.hours, + useConnectorSchedule: true, }); expect(AutomaticCrawlSchedulerLogic.values).toMatchObject({ @@ -127,22 +129,8 @@ describe('AutomaticCrawlSchedulerLogic', () => { describe('listeners', () => { describe('deleteCrawlSchedule', () => { - it('resets the states of the crawl scheduler and popover, and shows a toast, on success', async () => { - jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); - jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); - http.delete.mockReturnValueOnce(Promise.resolve()); - - AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); - await nextTick(); - - expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); - expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); - expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); - }); - describe('error paths', () => { - it('resets the states of the crawl scheduler and popover on a 404 respose', async () => { - jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); + it('resets the states of the crawl scheduler on a 404 response', async () => { jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); http.delete.mockReturnValueOnce( Promise.reject({ @@ -153,11 +141,10 @@ describe('AutomaticCrawlSchedulerLogic', () => { AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); await nextTick(); - expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); }); - it('flashes an error on a non-404 respose', async () => { + it('flashes an error on a non-404 response', async () => { jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); http.delete.mockReturnValueOnce( Promise.reject({ @@ -196,21 +183,7 @@ describe('AutomaticCrawlSchedulerLogic', () => { }); describe('error paths', () => { - it('resets the states of the crawl scheduler on a 404 respose', async () => { - jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); - http.get.mockReturnValueOnce( - Promise.reject({ - response: { status: 404 }, - }) - ); - - AutomaticCrawlSchedulerLogic.actions.fetchCrawlSchedule(); - await nextTick(); - - expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); - }); - - it('flashes an error on a non-404 respose', async () => { + it('flashes an error on a non-404 response', async () => { http.get.mockReturnValueOnce( Promise.reject({ response: { status: 500 }, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.ts index 51452dbbd581a..04a11c6c182e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/automatic_crawl_scheduler/automatic_crawl_scheduler_logic.ts @@ -7,11 +7,10 @@ import { kea, MakeLogicType } from 'kea'; -import { i18n } from '@kbn/i18n'; - -import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; +import { flashAPIErrors } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; -import { CrawlSchedule, CrawlUnits } from '../../../../api/crawler/types'; +import { CrawlSchedule, CrawlScheduleFromServer, CrawlUnits } from '../../../../api/crawler/types'; +import { crawlScheduleServerToClient } from '../../../../api/crawler/utils'; import { IndexNameLogic } from '../../index_name_logic'; export interface AutomaticCrawlSchedulerLogicValues { @@ -19,6 +18,7 @@ export interface AutomaticCrawlSchedulerLogicValues { crawlFrequency: CrawlSchedule['frequency']; crawlUnit: CrawlSchedule['unit']; isSubmitting: boolean; + useConnectorSchedule: CrawlSchedule['useConnectorSchedule']; } const DEFAULT_VALUES: Pick = { @@ -39,6 +39,9 @@ export interface AutomaticCrawlSchedulerLogicActions { }; setCrawlSchedule(crawlSchedule: CrawlSchedule): { crawlSchedule: CrawlSchedule }; setCrawlUnit(crawlUnit: CrawlSchedule['unit']): { crawlUnit: CrawlSchedule['unit'] }; + setUseConnectorSchedule(useConnectorSchedule: CrawlSchedule['useConnectorSchedule']): { + useConnectorSchedule: CrawlSchedule['useConnectorSchedule']; + }; submitCrawlSchedule(): void; toggleCrawlAutomatically(): void; } @@ -59,6 +62,7 @@ export const AutomaticCrawlSchedulerLogic = kea< submitCrawlSchedule: true, setCrawlFrequency: (crawlFrequency: string) => ({ crawlFrequency }), setCrawlUnit: (crawlUnit: CrawlUnits) => ({ crawlUnit }), + setUseConnectorSchedule: (useConnectorSchedule) => ({ useConnectorSchedule }), toggleCrawlAutomatically: true, }), reducers: () => ({ @@ -94,6 +98,14 @@ export const AutomaticCrawlSchedulerLogic = kea< submitCrawlSchedule: () => true, }, ], + useConnectorSchedule: [ + false, + { + setCrawlSchedule: (_, { crawlSchedule: { useConnectorSchedule = false } }) => + useConnectorSchedule, + setUseConnectorSchedule: (_, { useConnectorSchedule }) => useConnectorSchedule, + }, + ], }), listeners: ({ actions, values }) => ({ deleteCrawlSchedule: async () => { @@ -104,22 +116,10 @@ export const AutomaticCrawlSchedulerLogic = kea< await http.delete( `/internal/enterprise_search/indices/${indexName}/crawler/crawl_schedule` ); - actions.clearCrawlSchedule(); - flashSuccessToast( - i18n.translate( - 'xpack.enterpriseSearch.crawler.automaticCrawlScheduler.disableCrawlSchedule.successMessage', - { - defaultMessage: 'Automatic crawling has been disabled.', - } - ) - ); } catch (e) { // A 404 is expected and means the user has no crawl schedule to delete - if (e.response?.status === 404) { - actions.clearCrawlSchedule(); - } else { + if (e.response?.status !== 404) { flashAPIErrors(e); - // Keep the popover open } } finally { actions.onDoneSubmitting(); @@ -130,16 +130,14 @@ export const AutomaticCrawlSchedulerLogic = kea< const { indexName } = IndexNameLogic.values; try { - const crawlSchedule: CrawlSchedule = await http.get( + const crawlSchedule: CrawlScheduleFromServer = await http.get( `/internal/enterprise_search/indices/${indexName}/crawler/crawl_schedule` ); - actions.setCrawlSchedule(crawlSchedule); + actions.setCrawlSchedule(crawlScheduleServerToClient(crawlSchedule)); } catch (e) { // A 404 is expected and means the user does not have crawl schedule // for this index. We continue to use the defaults. - if (e.response?.status === 404) { - actions.clearCrawlSchedule(); - } else { + if (e.response?.status !== 404) { flashAPIErrors(e); } } @@ -151,29 +149,30 @@ export const AutomaticCrawlSchedulerLogic = kea< actions.deleteCrawlSchedule(); } }, + setCrawlUnit: actions.saveChanges, + setCrawlFrequency: actions.saveChanges, + setUseConnectorSchedule: actions.saveChanges, + toggleCrawlAutomatically: actions.saveChanges, submitCrawlSchedule: async () => { const { http } = HttpLogic.values; const { indexName } = IndexNameLogic.values; + if (!values.crawlUnit || !values.crawlFrequency) { + return; + } + try { - const crawlSchedule: CrawlSchedule = await http.put( + const crawlSchedule: CrawlScheduleFromServer = await http.put( `/internal/enterprise_search/indices/${indexName}/crawler/crawl_schedule`, { body: JSON.stringify({ unit: values.crawlUnit, frequency: values.crawlFrequency, + use_connector_schedule: values.useConnectorSchedule, }), } ); - actions.setCrawlSchedule(crawlSchedule); - flashSuccessToast( - i18n.translate( - 'xpack.enterpriseSearch.crawler.automaticCrawlScheduler.submitCrawlSchedule.successMessage', - { - defaultMessage: 'Your automatic crawling schedule has been updated.', - } - ) - ); + actions.setCrawlSchedule(crawlScheduleServerToClient(crawlSchedule)); } catch (e) { flashAPIErrors(e); } finally { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx index 193df13c3ad98..822add8f8bde3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx @@ -61,9 +61,17 @@ export const SyncJobs: React.FC = () => { truncateText: true, }, { - field: 'docsCount', - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle', { - defaultMessage: 'Docs count', + field: 'indexed_document_count', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.addedDocs.columnTitle', { + defaultMessage: 'Docs added', + }), + sortable: true, + truncateText: true, + }, + { + field: 'deleted_document_count', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.deletedDocs.columnTitle', { + defaultMessage: 'Docs deleted', }), sortable: true, truncateText: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/language_to_text.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/language_to_text.ts new file mode 100644 index 0000000000000..34b7f9fdcd7ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/utils/language_to_text.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIVERSAL_LANGUAGE_VALUE = ''; + +export const languageToTextMap: Record = { + [UNIVERSAL_LANGUAGE_VALUE]: i18n.translate( + 'xpack.enterpriseSearch.content.supportedLanguages.universalLabel', + { + defaultMessage: 'Universal', + } + ), + da: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.danishLabel', { + defaultMessage: 'Danish', + }), + de: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.germanLabel', { + defaultMessage: 'German', + }), + en: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.englishLabel', { + defaultMessage: 'English', + }), + es: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.spanishLabel', { + defaultMessage: 'Spanish', + }), + + fr: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.frenchLabel', { + defaultMessage: 'French', + }), + + it: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.italianLabel', { + defaultMessage: 'Italian', + }), + ja: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.japaneseLabel', { + defaultMessage: 'Japanese', + }), + ko: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.koreanLabel', { + defaultMessage: 'Korean', + }), + + nl: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.dutchLabel', { + defaultMessage: 'Dutch', + }), + pt: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.portugueseLabel', { + defaultMessage: 'Portuguese', + }), + 'pt-br': i18n.translate( + 'xpack.enterpriseSearch.content.supportedLanguages.portugueseBrazilLabel', + { + defaultMessage: 'Portuguese (Brazil)', + } + ), + ru: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.russianLabel', { + defaultMessage: 'Russian', + }), + th: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.thaiLabel', { + defaultMessage: 'Thai', + }), + zh: i18n.translate('xpack.enterpriseSearch.content.supportedLanguages.chineseLabel', { + defaultMessage: 'Chinese', + }), +}; + +export function languageToText(input: string): string { + return languageToTextMap[input] ?? input; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/constants.ts new file mode 100644 index 0000000000000..ad0c2bb45cc39 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/constants.ts @@ -0,0 +1,155 @@ +/* + * 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 { padStart } from 'lodash'; + +import { EuiSelectOption } from '@elastic/eui'; + +import { DayOrdinal, MonthOrdinal, getOrdinalValue, getDayName, getMonthName } from './services'; +import { Frequency, Field, FieldToValueMap } from './types'; + +type FieldFlags = { + [key in Field]?: boolean; +}; + +function makeSequence(min: number, max: number): number[] { + const values = []; + for (let i = min; i <= max; i++) { + values.push(i); + } + return values; +} + +export const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({ + value: value.toString(), + text: padStart(value.toString(), 2, '0'), +})); + +export const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({ + value: value.toString(), + text: padStart(value.toString(), 2, '0'), +})); + +export const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({ + value: value.toString(), + text: getDayName((value - 1) as DayOrdinal), +})); + +export const DATE_OPTIONS = makeSequence(1, 31).map((value) => ({ + value: value.toString(), + text: getOrdinalValue(value), +})); + +export const MONTH_OPTIONS = makeSequence(1, 12).map((value) => ({ + value: value.toString(), + text: getMonthName((value - 1) as MonthOrdinal), +})); + +export const UNITS: EuiSelectOption[] = [ + { + value: 'MINUTE', + text: 'minute', + }, + { + value: 'HOUR', + text: 'hour', + }, + { + value: 'DAY', + text: 'day', + }, + { + value: 'WEEK', + text: 'week', + }, + { + value: 'MONTH', + text: 'month', + }, + { + value: 'YEAR', + text: 'year', + }, +]; + +export const frequencyToFieldsMap: Record = { + MINUTE: {}, + HOUR: { + minute: true, + }, + DAY: { + hour: true, + minute: true, + }, + WEEK: { + day: true, + hour: true, + minute: true, + }, + MONTH: { + date: true, + hour: true, + minute: true, + }, + YEAR: { + month: true, + date: true, + hour: true, + minute: true, + }, +}; + +export const frequencyToBaselineFieldsMap: Record = { + MINUTE: { + second: '0', + minute: '*', + hour: '*', + date: '*', + month: '*', + day: '?', + }, + HOUR: { + second: '0', + minute: '0', + hour: '*', + date: '*', + month: '*', + day: '?', + }, + DAY: { + second: '0', + minute: '0', + hour: '0', + date: '*', + month: '*', + day: '?', + }, + WEEK: { + second: '0', + minute: '0', + hour: '0', + date: '?', + month: '*', + day: '7', + }, + MONTH: { + second: '0', + minute: '0', + hour: '0', + date: '1', + month: '*', + day: '?', + }, + YEAR: { + second: '0', + minute: '0', + hour: '0', + date: '1', + month: '1', + day: '?', + }, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_daily.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_daily.tsx new file mode 100644 index 0000000000000..5fb736fa26395 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_daily.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, { Fragment } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + disabled?: boolean; + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + onChange: ({ minute, hour }: { minute?: string; hour?: string }) => void; +} + +export const CronDaily: React.FunctionComponent = ({ + disabled, + minute, + minuteOptions, + hour, + hourOptions, + onChange, +}) => ( + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + + + onChange({ hour: e.target.value })} + fullWidth + prepend={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronDaily.fieldHour.textAtLabel', + { + defaultMessage: 'At', + } + )} + data-test-subj="cronFrequencyDailyHourSelect" + /> + + + + onChange({ minute: e.target.value })} + fullWidth + prepend=":" + data-test-subj="cronFrequencyDailyMinuteSelect" + /> + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.test.tsx new file mode 100644 index 0000000000000..5ab99c715453b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.test.tsx @@ -0,0 +1,121 @@ +/* + * 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 sinon from 'sinon'; + +import { findTestSubject } from '@elastic/eui/lib/test'; +import { Frequency } from '@kbn/es-ui-shared-plugin/public/components/cron_editor/types'; +import { mountWithI18nProvider } from '@kbn/test-jest-helpers'; + +import { CronEditor } from './cron_editor'; + +describe('CronEditor', () => { + ['MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'].forEach((unit) => { + test(`is rendered with a ${unit} frequency`, () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + }); + + describe('props', () => { + describe('frequencyBlockList', () => { + it('excludes the blocked frequencies from the frequency list', () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); + expect(frequencySelect.text()).toBe('minutedaymonth'); + }); + }); + + describe('cronExpression', () => { + it('sets the values of the fields', () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + const monthSelect = findTestSubject(component, 'cronFrequencyYearlyMonthSelect'); + expect(monthSelect.props().value).toBe('2'); + + const dateSelect = findTestSubject(component, 'cronFrequencyYearlyDateSelect'); + expect(dateSelect.props().value).toBe('5'); + + const hourSelect = findTestSubject(component, 'cronFrequencyYearlyHourSelect'); + expect(hourSelect.props().value).toBe('10'); + + const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); + expect(minuteSelect.props().value).toBe('20'); + }); + }); + + describe('onChange', () => { + it('is called when the frequency changes', () => { + const onChangeSpy = sinon.spy(); + const component = mountWithI18nProvider( + + ); + + const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); + frequencySelect.simulate('change', { target: { value: 'MONTH' } }); + + sinon.assert.calledWith(onChangeSpy, { + cronExpression: '0 0 0 1 * ?', + fieldToPreferredValueMap: {}, + frequency: 'MONTH', + }); + }); + + it("is called when a field's value changes", () => { + const onChangeSpy = sinon.spy(); + const component = mountWithI18nProvider( + + ); + + const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); + minuteSelect.simulate('change', { target: { value: '40' } }); + + sinon.assert.calledWith(onChangeSpy, { + cronExpression: '0 40 * * * ?', + fieldToPreferredValueMap: { minute: '40' }, + frequency: 'YEAR', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.tsx new file mode 100644 index 0000000000000..06e37aa2366f8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.tsx @@ -0,0 +1,253 @@ +/* + * 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, { Component, Fragment } from 'react'; + +import { EuiSelect, EuiFormRow, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + MINUTE_OPTIONS, + HOUR_OPTIONS, + DAY_OPTIONS, + DATE_OPTIONS, + MONTH_OPTIONS, + UNITS, + frequencyToFieldsMap, + frequencyToBaselineFieldsMap, +} from './constants'; +import { CronDaily } from './cron_daily'; +import { CronHourly } from './cron_hourly'; +import { CronMonthly } from './cron_monthly'; +import { CronWeekly } from './cron_weekly'; +import { CronYearly } from './cron_yearly'; +import { cronExpressionToParts, cronPartsToExpression } from './services'; +import { Frequency, Field, FieldToValueMap } from './types'; + +const excludeBlockListedFrequencies = ( + units: EuiSelectOption[], + blockListedUnits: string[] = [] +): EuiSelectOption[] => { + if (blockListedUnits.length === 0) { + return units; + } + + return units.filter(({ value }) => !blockListedUnits.includes(value as string)); +}; + +interface Props { + frequencyBlockList?: string[]; + fieldToPreferredValueMap: FieldToValueMap; + frequency: Frequency; + cronExpression: string; + onChange: ({ + cronExpression, + fieldToPreferredValueMap, + frequency, + }: { + cronExpression: string; + fieldToPreferredValueMap: FieldToValueMap; + frequency: Frequency; + }) => void; + autoFocus?: boolean; + disabled?: boolean; +} + +type State = FieldToValueMap; + +export class CronEditor extends Component { + static getDerivedStateFromProps(props: Props) { + const { cronExpression } = props; + return cronExpressionToParts(cronExpression); + } + + constructor(props: Props) { + super(props); + + const { cronExpression } = props; + const parsedCron = cronExpressionToParts(cronExpression); + this.state = { + ...parsedCron, + }; + } + + onChangeFrequency = (frequency: Frequency) => { + const { onChange, fieldToPreferredValueMap } = this.props; + + // Update fields which aren't editable with acceptable baseline values. + const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[]; + const inheritedFields = editableFields.reduce( + (fieldBaselines, field) => { + if (fieldToPreferredValueMap[field] != null) { + fieldBaselines[field] = fieldToPreferredValueMap[field]; + } + return fieldBaselines; + }, + { ...frequencyToBaselineFieldsMap[frequency] } + ); + + const newCronExpression = cronPartsToExpression(inheritedFields); + + onChange({ + frequency, + cronExpression: newCronExpression, + fieldToPreferredValueMap, + }); + }; + + onChangeFields = (fields: FieldToValueMap) => { + const { onChange, frequency, fieldToPreferredValueMap } = this.props; + + const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[]; + const newFieldToPreferredValueMap: FieldToValueMap = {}; + + const editedFields = editableFields.reduce( + (accumFields, field) => { + if (fields[field] !== undefined) { + accumFields[field] = fields[field]; + // If the user changes a field's value, we want to maintain that value in the relevant + // field, even as the frequency field changes. For example, if the user selects "Monthly" + // frequency and changes the "Hour" field to "10", that field should still say "10" if the + // user changes the frequency to "Weekly". We'll support this UX by storing these values + // in the fieldToPreferredValueMap. + newFieldToPreferredValueMap[field] = fields[field]; + } else { + accumFields[field] = this.state[field]; + } + return accumFields; + }, + { ...frequencyToBaselineFieldsMap[frequency] } + ); + + const newCronExpression = cronPartsToExpression(editedFields); + + onChange({ + frequency, + cronExpression: newCronExpression, + fieldToPreferredValueMap: { + ...fieldToPreferredValueMap, + ...newFieldToPreferredValueMap, + }, + }); + }; + + renderForm() { + const { frequency, disabled } = this.props; + + const { minute, hour, day, date, month } = this.state; + + switch (frequency) { + case 'MINUTE': + return; + + case 'HOUR': + return ( + + ); + + case 'DAY': + return ( + + ); + + case 'WEEK': + return ( + + ); + + case 'MONTH': + return ( + + ); + + case 'YEAR': + return ( + + ); + + default: + return; + } + } + + render() { + const { disabled, frequency, frequencyBlockList } = this.props; + + return ( + + + } + fullWidth + > + ) => + this.onChangeFrequency(e.target.value as Frequency) + } + fullWidth + prepend={i18n.translate('xpack.enterpriseSearch.cronEditor.textEveryLabel', { + defaultMessage: 'Every', + })} + data-test-subj="cronFrequencySelect" + /> + + + {this.renderForm()} + + ); + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_hourly.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_hourly.tsx new file mode 100644 index 0000000000000..84ceab265e624 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_hourly.tsx @@ -0,0 +1,54 @@ +/* + * 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, { Fragment } from 'react'; + +import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + disabled?: boolean; + minute?: string; + minuteOptions: EuiSelectOption[]; + onChange: ({ minute }: { minute?: string }) => void; +} + +export const CronHourly: React.FunctionComponent = ({ + disabled, + minute, + minuteOptions, + onChange, +}) => ( + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + onChange({ minute: e.target.value })} + fullWidth + prepend={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronHourly.fieldMinute.textAtLabel', + { + defaultMessage: 'At', + } + )} + data-test-subj="cronFrequencyHourlyMinuteSelect" + /> + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_monthly.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_monthly.tsx new file mode 100644 index 0000000000000..84867e5bbf893 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_monthly.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 React, { Fragment } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + disabled?: boolean; + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + date?: string; + dateOptions: EuiSelectOption[]; + onChange: ({ minute, hour, date }: { minute?: string; hour?: string; date?: string }) => void; +} + +export const CronMonthly: React.FunctionComponent = ({ + disabled, + minute, + minuteOptions, + hour, + hourOptions, + date, + dateOptions, + onChange, +}) => ( + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + onChange({ date: e.target.value })} + fullWidth + prepend={i18n.translate('xpack.enterpriseSearch.cronEditor.cronMonthly.textOnTheLabel', { + defaultMessage: 'On the', + })} + data-test-subj="cronFrequencyMonthlyDateSelect" + /> + + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + + + onChange({ hour: e.target.value })} + fullWidth + prepend={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronMonthly.fieldHour.textAtLabel', + { + defaultMessage: 'At', + } + )} + data-test-subj="cronFrequencyMonthlyHourSelect" + /> + + + + onChange({ minute: e.target.value })} + fullWidth + prepend=":" + data-test-subj="cronFrequencyMonthlyMinuteSelect" + /> + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_weekly.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_weekly.tsx new file mode 100644 index 0000000000000..83edd0ed0aaba --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_weekly.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 React, { Fragment } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + disabled?: boolean; + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + day?: string; + dayOptions: EuiSelectOption[]; + onChange: ({ minute, hour, day }: { minute?: string; hour?: string; day?: string }) => void; +} + +export const CronWeekly: React.FunctionComponent = ({ + disabled, + minute, + minuteOptions, + hour, + hourOptions, + day, + dayOptions, + onChange, +}) => ( + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + onChange({ day: e.target.value })} + fullWidth + prepend={i18n.translate('xpack.enterpriseSearch.cronEditor.cronWeekly.textOnLabel', { + defaultMessage: 'On', + })} + data-test-subj="cronFrequencyWeeklyDaySelect" + /> + + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + + + onChange({ hour: e.target.value })} + fullWidth + prepend={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronWeekly.fieldHour.textAtLabel', + { + defaultMessage: 'At', + } + )} + data-test-subj="cronFrequencyWeeklyHourSelect" + /> + + + + onChange({ minute: e.target.value })} + aria-label={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronWeekly.minuteSelectLabel', + { + defaultMessage: 'Minute', + } + )} + fullWidth + prepend=":" + data-test-subj="cronFrequencyWeeklyMinuteSelect" + /> + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_yearly.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_yearly.tsx new file mode 100644 index 0000000000000..158ab6dcac79d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_yearly.tsx @@ -0,0 +1,156 @@ +/* + * 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, { Fragment } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + disabled?: boolean; + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + date?: string; + dateOptions: EuiSelectOption[]; + month?: string; + monthOptions: EuiSelectOption[]; + onChange: ({ + minute, + hour, + date, + month, + }: { + minute?: string; + hour?: string; + date?: string; + month?: string; + }) => void; +} + +export const CronYearly: React.FunctionComponent = ({ + disabled, + minute, + minuteOptions, + hour, + hourOptions, + date, + dateOptions, + month, + monthOptions, + onChange, +}) => ( + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + onChange({ month: e.target.value })} + fullWidth + prepend={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronYearly.fieldMonth.textInLabel', + { + defaultMessage: 'In', + } + )} + data-test-subj="cronFrequencyYearlyMonthSelect" + /> + + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + onChange({ date: e.target.value })} + fullWidth + prepend={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronYearly.fieldDate.textOnTheLabel', + { + defaultMessage: 'On the', + } + )} + data-test-subj="cronFrequencyYearlyDateSelect" + /> + + + + } + fullWidth + data-test-subj="cronFrequencyConfiguration" + > + + + onChange({ hour: e.target.value })} + fullWidth + prepend={i18n.translate( + 'xpack.enterpriseSearch.cronEditor.cronYearly.fieldHour.textAtLabel', + { + defaultMessage: 'At', + } + )} + data-test-subj="cronFrequencyYearlyHourSelect" + /> + + + + onChange({ minute: e.target.value })} + fullWidth + prepend=":" + data-test-subj="cronFrequencyYearlyMinuteSelect" + /> + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/enterprise_search_cron_editor.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/enterprise_search_cron_editor.tsx new file mode 100644 index 0000000000000..f0e2d371dc081 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/enterprise_search_cron_editor.tsx @@ -0,0 +1,76 @@ +/* + * 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 { Frequency } from '@kbn/es-ui-shared-plugin/public/components/cron_editor/types'; + +import { Connector } from '../../../../common/types/connectors'; + +import { CronEditor } from './cron_editor'; + +interface Props { + disabled?: boolean; + onChange(scheduling: Connector['scheduling']): void; + scheduling: Connector['scheduling']; +} + +export const EnterpriseSearchCronEditor: React.FC = ({ disabled, onChange, scheduling }) => { + const [fieldToPreferredValueMap, setFieldToPreferredValueMap] = useState({}); + const [simpleCron, setSimpleCron] = useState<{ + expression: string; + frequency: Frequency; + }>({ + expression: scheduling?.interval ?? '', + frequency: scheduling?.interval ? cronToFrequency(scheduling.interval) : 'HOUR', + }); + + return ( + { + setSimpleCron({ + expression, + frequency, + }); + setFieldToPreferredValueMap(newFieldToPreferredValueMap); + onChange({ ...scheduling, interval: expression }); + }} + frequencyBlockList={['MINUTE']} + /> + ); +}; + +function cronToFrequency(cron: string): Frequency { + const fields = cron.split(' '); + if (fields.length < 4) { + return 'YEAR'; + } + if (fields[1] === '*') { + return 'MINUTE'; + } + if (fields[2] === '*') { + return 'HOUR'; + } + if (fields[3] === '*') { + return 'DAY'; + } + if (fields[4] === '?') { + return 'WEEK'; + } + if (fields[4] === '*') { + return 'MONTH'; + } + return 'YEAR'; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/index.ts new file mode 100644 index 0000000000000..981521acf886b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/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 { CronEditor } from './cron_editor'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/readme.md b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/readme.md new file mode 100644 index 0000000000000..1b2f8e39e9e58 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/readme.md @@ -0,0 +1,5 @@ +`CronEditor` found `./cron_editor.tsx` is based on the `Cron Editor` component from `src/plugins/es_ui_shared/public/components/cron_editor` + +Includes a `disabled` prop that can be passed down to the child form components. + +TODO: PR this prop back to the original ES UI component diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/cron.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/cron.ts new file mode 100644 index 0000000000000..542502fbcbe76 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/cron.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldToValueMap } from '../types'; + +export function cronExpressionToParts(expression: string): FieldToValueMap { + const parsedCron: FieldToValueMap = { + second: undefined, + minute: undefined, + hour: undefined, + day: undefined, + date: undefined, + month: undefined, + }; + + const parts = expression.split(' '); + + if (parts.length >= 1) { + parsedCron.second = parts[0]; + } + + if (parts.length >= 2) { + parsedCron.minute = parts[1]; + } + + if (parts.length >= 3) { + parsedCron.hour = parts[2]; + } + + if (parts.length >= 4) { + parsedCron.date = parts[3]; + } + + if (parts.length >= 5) { + parsedCron.month = parts[4]; + } + + if (parts.length >= 6) { + parsedCron.day = parts[5]; + } + + return parsedCron; +} + +export function cronPartsToExpression({ + second, + minute, + hour, + day, + date, + month, +}: FieldToValueMap): string { + return `${second} ${minute} ${hour} ${date} ${month} ${day}`; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/humanized_numbers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/humanized_numbers.ts new file mode 100644 index 0000000000000..e169a76ec8b41 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/humanized_numbers.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export type DayOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6; +export type MonthOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; + +// The international ISO standard dictates Monday as the first day of the week, but cron patterns +// use Sunday as the first day, so we're going with the cron way. +const dayOrdinalToDayNameMap = { + 0: i18n.translate('xpack.enterpriseSearch.cronEditor.day.sunday', { defaultMessage: 'Sunday' }), + 1: i18n.translate('xpack.enterpriseSearch.cronEditor.day.monday', { defaultMessage: 'Monday' }), + 2: i18n.translate('xpack.enterpriseSearch.cronEditor.day.tuesday', { defaultMessage: 'Tuesday' }), + 3: i18n.translate('xpack.enterpriseSearch.cronEditor.day.wednesday', { + defaultMessage: 'Wednesday', + }), + 4: i18n.translate('xpack.enterpriseSearch.cronEditor.day.thursday', { + defaultMessage: 'Thursday', + }), + 5: i18n.translate('xpack.enterpriseSearch.cronEditor.day.friday', { defaultMessage: 'Friday' }), + 6: i18n.translate('xpack.enterpriseSearch.cronEditor.day.saturday', { + defaultMessage: 'Saturday', + }), +}; + +const monthOrdinalToMonthNameMap = { + 0: i18n.translate('xpack.enterpriseSearch.cronEditor.month.january', { + defaultMessage: 'January', + }), + 1: i18n.translate('xpack.enterpriseSearch.cronEditor.month.february', { + defaultMessage: 'February', + }), + 2: i18n.translate('xpack.enterpriseSearch.cronEditor.month.march', { defaultMessage: 'March' }), + 3: i18n.translate('xpack.enterpriseSearch.cronEditor.month.april', { defaultMessage: 'April' }), + 4: i18n.translate('xpack.enterpriseSearch.cronEditor.month.may', { defaultMessage: 'May' }), + 5: i18n.translate('xpack.enterpriseSearch.cronEditor.month.june', { defaultMessage: 'June' }), + 6: i18n.translate('xpack.enterpriseSearch.cronEditor.month.july', { defaultMessage: 'July' }), + 7: i18n.translate('xpack.enterpriseSearch.cronEditor.month.august', { defaultMessage: 'August' }), + 8: i18n.translate('xpack.enterpriseSearch.cronEditor.month.september', { + defaultMessage: 'September', + }), + 9: i18n.translate('xpack.enterpriseSearch.cronEditor.month.october', { + defaultMessage: 'October', + }), + 10: i18n.translate('xpack.enterpriseSearch.cronEditor.month.november', { + defaultMessage: 'November', + }), + 11: i18n.translate('xpack.enterpriseSearch.cronEditor.month.december', { + defaultMessage: 'December', + }), +}; + +export function getOrdinalValue(number: number): string { + // TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale, + // which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings. + // return i18n.translate('xpack.enterpriseSearch.cronEditor.number.ordinal', { + // defaultMessage: '{number, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}', + // values: { number }, + // }); + // TODO: https://github.com/elastic/kibana/issues/27136 + + // Protects against falsey (including 0) values + const num = number && number.toString(); + const lastDigitString = num && num.substr(-1); + let ordinal; + + if (!lastDigitString) { + return number.toString(); + } + + const lastDigitNumeric = parseFloat(lastDigitString); + + switch (lastDigitNumeric) { + case 1: + ordinal = 'st'; + break; + case 2: + ordinal = 'nd'; + break; + case 3: + ordinal = 'rd'; + break; + default: + ordinal = 'th'; + } + + return `${num}${ordinal}`; +} + +export function getDayName(dayOrdinal: DayOrdinal): string { + return dayOrdinalToDayNameMap[dayOrdinal]; +} + +export function getMonthName(monthOrdinal: MonthOrdinal): string { + return monthOrdinalToMonthNameMap[monthOrdinal]; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/index.ts new file mode 100644 index 0000000000000..d8fcdd3382274 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/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 { cronExpressionToParts, cronPartsToExpression } from './cron'; +export type { DayOrdinal, MonthOrdinal } from './humanized_numbers'; +export { getOrdinalValue, getDayName, getMonthName } from './humanized_numbers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/types.ts new file mode 100644 index 0000000000000..a7b2d7b5b63e7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type Frequency = 'MINUTE' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'YEAR'; +export type Field = 'second' | 'minute' | 'hour' | 'day' | 'date' | 'month'; +export type FieldToValueMap = { + [key in Field]?: string; +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.test.ts index 3947e569349c8..89c01bdd8fd7f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.test.ts @@ -594,7 +594,7 @@ describe('crawler routes', () => { it('validates correctly', () => { const request = { - body: { frequency: 7, unit: 'day' }, + body: { frequency: 7, unit: 'day', use_connector_schedule: true }, params: { indexName: 'index-name' }, }; mockRouter.shouldValidate(request); @@ -602,7 +602,7 @@ describe('crawler routes', () => { it('fails validation without a name param', () => { const request = { - body: { frequency: 7, unit: 'day' }, + body: { frequency: 7, unit: 'day', use_connector_schedule: true }, params: {}, }; mockRouter.shouldThrow(request); @@ -610,7 +610,7 @@ describe('crawler routes', () => { it('fails validation without a unit property in body', () => { const request = { - body: { frequency: 7 }, + body: { frequency: 7, use_connector_schedule: true }, params: { indexName: 'index-name' }, }; mockRouter.shouldThrow(request); @@ -618,7 +618,15 @@ describe('crawler routes', () => { it('fails validation without a frequency property in body', () => { const request = { - body: { unit: 'day' }, + body: { unit: 'day', use_connector_schedule: true }, + params: { indexName: 'index-name' }, + }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without a use_connector_schedule property in body', () => { + const request = { + body: { frequency: 7, unit: 'day' }, params: { indexName: 'index-name' }, }; mockRouter.shouldThrow(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts index 0af97578b56a4..c3b034f0b6ce7 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/crawler/crawler.ts @@ -363,6 +363,7 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) { body: schema.object({ frequency: schema.number(), unit: schema.string(), + use_connector_schedule: schema.boolean(), }), params: schema.object({ indexName: schema.string(), diff --git a/x-pack/plugins/fleet/common/constants/file_storage.ts b/x-pack/plugins/fleet/common/constants/file_storage.ts index a1988570a5873..24994a28c9128 100644 --- a/x-pack/plugins/fleet/common/constants/file_storage.ts +++ b/x-pack/plugins/fleet/common/constants/file_storage.ts @@ -10,3 +10,12 @@ // found in `common/services/file_storage` export const FILE_STORAGE_METADATA_INDEX_PATTERN = '.fleet-files-*'; export const FILE_STORAGE_DATA_INDEX_PATTERN = '.fleet-file-data-*'; + +// which integrations support file upload and the name to use for the file upload index +export const FILE_STORAGE_INTEGRATION_INDEX_NAMES: Readonly> = { + elastic_agent: 'agent', + endpoint: 'endpoint', +}; +export const FILE_STORAGE_INTEGRATION_NAMES: Readonly = Object.keys( + FILE_STORAGE_INTEGRATION_INDEX_NAMES +); diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index 2c55ce4a2a08e..e7379ba1e2a4b 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -15,7 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ createPackagePolicyMultiPageLayout: true, packageVerification: true, showDevtoolsRequest: true, - showRequestDiagnostics: false, + diagnosticFileUploadEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/fleet/common/services/file_storage.ts b/x-pack/plugins/fleet/common/services/file_storage.ts index 4c5e9ac204c58..afabb421e7d45 100644 --- a/x-pack/plugins/fleet/common/services/file_storage.ts +++ b/x-pack/plugins/fleet/common/services/file_storage.ts @@ -34,6 +34,11 @@ export const getFileDataIndexName = (integrationName: string): string => { ); }; +/** + * Returns the write index name for a given file upload alias name, this is the same for metadata and chunks + * @param aliasName + */ +export const getFileWriteIndexName = (aliasName: string) => aliasName + '-000001'; /** * Returns back the integration name for a given File Data (chunks) index name. * @@ -63,3 +68,15 @@ export const getIntegrationNameFromFileDataIndexName = (indexName: string): stri throw new Error(`Index name ${indexName} does not seem to be a File Data storage index`); }; + +export const getFileStorageWriteIndexBody = (aliasName: string) => ({ + aliases: { + [aliasName]: { + is_write_index: true, + }, + }, + settings: { + 'index.lifecycle.rollover_alias': aliasName, + 'index.hidden': true, + }, +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx index b52a0401b8650..76bcd21c0207c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx @@ -38,7 +38,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ const isUnenrolling = agent.status === 'unenrolling'; const hasFleetServer = agentPolicy && policyHasFleetServer(agentPolicy); - const { showRequestDiagnostics } = ExperimentalFeaturesService.get(); + const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get(); const onClose = useMemo(() => { if (onCancelReassign) { @@ -95,7 +95,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ , ]; - if (showRequestDiagnostics) { + if (diagnosticFileUploadEnabled) { menuItems.push( { navigateToApp(routeState.onDoneNavigateTo[0], routeState.onDoneNavigateTo[1]); } }, [routeState, navigateToApp]); - const { showRequestDiagnostics } = ExperimentalFeaturesService.get(); + const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get(); const host = agentData?.item?.local_metadata?.host; @@ -156,7 +156,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { isSelected: tabId === 'logs', }, ]; - if (showRequestDiagnostics) { + if (diagnosticFileUploadEnabled) { tabs.push({ id: 'diagnostics', name: i18n.translate('xpack.fleet.agentDetails.subTabs.diagnosticsTab', { @@ -167,7 +167,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { }); } return tabs; - }, [getHref, agentId, tabId, showRequestDiagnostics]); + }, [getHref, agentId, tabId, diagnosticFileUploadEnabled]); return ( = ({ const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents; const agents = selectionMode === 'manual' ? selectedAgents : currentQuery; const [tagsPopoverButton, setTagsPopoverButton] = useState(); - const { showRequestDiagnostics } = ExperimentalFeaturesService.get(); + const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get(); const menuItems = [ { @@ -171,7 +171,7 @@ export const AgentBulkActions: React.FunctionComponent = ({ }, ]; - if (showRequestDiagnostics) { + if (diagnosticFileUploadEnabled) { menuItems.push({ name: ( { createPackagePolicyMultiPageLayout: true, packageVerification: true, showDevtoolsRequest: false, - showRequestDiagnostics: false, + diagnosticFileUploadEnabled: false, }); }); it('should show no Actions button when no agent is selected', async () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_row_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_row_actions.tsx index ad9c9bdbf3f7b..6fdf4016d457f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_row_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_row_actions.tsx @@ -38,7 +38,7 @@ export const TableRowActions: React.FunctionComponent<{ const isUnenrolling = agent.status === 'unenrolling'; const kibanaVersion = useKibanaVersion(); const [isMenuOpen, setIsMenuOpen] = useState(false); - const { showRequestDiagnostics } = ExperimentalFeaturesService.get(); + const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get(); const menuItems = [ ); - if (showRequestDiagnostics) { + if (diagnosticFileUploadEnabled) { menuItems.push( { createPackagePolicyMultiPageLayout: true, packageVerification: true, showDevtoolsRequest: false, - showRequestDiagnostics: false, + diagnosticFileUploadEnabled: false, }); const HookWrapper = memo(({ children }) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 14a773cfd94bf..ca03d272405f4 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -9,8 +9,21 @@ import { merge, concat, uniqBy, omit } from 'lodash'; import Boom from '@hapi/boom'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { IndicesCreateRequest } from '@elastic/elasticsearch/lib/api/types'; + +import { + FILE_STORAGE_INTEGRATION_INDEX_NAMES, + FILE_STORAGE_INTEGRATION_NAMES, +} from '../../../../../common/constants'; + import { ElasticsearchAssetType } from '../../../../types'; -import { getPipelineNameForDatastream } from '../../../../../common/services'; +import { + getFileWriteIndexName, + getFileStorageWriteIndexBody, + getPipelineNameForDatastream, + getFileDataIndexName, + getFileMetadataIndexName, +} from '../../../../../common/services'; import type { RegistryDataStream, IndexTemplateEntry, @@ -341,6 +354,43 @@ export async function ensureDefaultComponentTemplates( ); } +/* + * Given a list of integration names, if the integrations support file upload + * then ensure that the alias has a matching write index, as we use "plain" indices + * not data streams. + * e.g .fleet-file-data-agent must have .fleet-file-data-agent-00001 as the write index + * before files can be uploaded. + */ +export async function ensureFileUploadWriteIndices(opts: { + esClient: ElasticsearchClient; + logger: Logger; + integrationNames: string[]; +}) { + const { esClient, logger, integrationNames } = opts; + + const integrationsWithFileUpload = integrationNames.filter((integration) => + FILE_STORAGE_INTEGRATION_NAMES.includes(integration as any) + ); + + if (!integrationsWithFileUpload.length) return []; + + const ensure = (aliasName: string) => + ensureAliasHasWriteIndex({ + esClient, + logger, + aliasName, + writeIndexName: getFileWriteIndexName(aliasName), + body: getFileStorageWriteIndexBody(aliasName), + }); + + return Promise.all( + integrationsWithFileUpload.flatMap((integrationName) => { + const indexName = FILE_STORAGE_INTEGRATION_INDEX_NAMES[integrationName]; + return [ensure(getFileDataIndexName(indexName)), ensure(getFileMetadataIndexName(indexName))]; + }) + ); +} + export async function ensureComponentTemplate( esClient: ElasticsearchClient, logger: Logger, @@ -371,6 +421,37 @@ export async function ensureComponentTemplate( return { isCreated: !existingTemplate }; } +export async function ensureAliasHasWriteIndex(opts: { + esClient: ElasticsearchClient; + logger: Logger; + aliasName: string; + writeIndexName: string; + body: Omit; +}): Promise { + const { esClient, logger, aliasName, writeIndexName, body } = opts; + const existingIndex = await retryTransientEsErrors( + () => + esClient.indices.exists( + { + index: [aliasName], + }, + { + ignore: [404], + } + ), + { logger } + ); + + if (!existingIndex) { + await retryTransientEsErrors( + () => esClient.indices.create({ index: writeIndexName, ...body }, { ignore: [404] }), + { + logger, + } + ); + } +} + export function prepareTemplate({ pkg, dataStream, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 78683ecd07e0a..4b0aed34f8520 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -32,7 +32,10 @@ import type { PackageAssetReference, PackageVerificationResult, } from '../../../types'; -import { prepareToInstallTemplates } from '../elasticsearch/template/install'; +import { + ensureFileUploadWriteIndices, + prepareToInstallTemplates, +} from '../elasticsearch/template/install'; import { removeLegacyTemplates } from '../elasticsearch/template/remove_legacy'; import { prepareToInstallPipelines, @@ -47,7 +50,7 @@ import { installMlModel } from '../elasticsearch/ml_model'; import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install'; import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; -import { packagePolicyService } from '../..'; +import { appContextService, packagePolicyService } from '../..'; import { createInstallation, updateEsAssetReferences, restartInstallation } from './install'; import { withPackageSpan } from './utils'; @@ -214,6 +217,15 @@ export async function _installPackage({ logger.warn(`Error removing legacy templates: ${e.message}`); } + const { diagnosticFileUploadEnabled } = appContextService.getExperimentalFeatures(); + if (diagnosticFileUploadEnabled) { + await ensureFileUploadWriteIndices({ + integrationNames: [packageInfo.name], + esClient, + logger, + }); + } + // update current backing indices of each data stream await withPackageSpan('Update write indices', () => updateCurrentWriteIndices(esClient, logger, installedTemplates) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 3c4861b563b08..82c3175583a75 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -330,6 +330,18 @@ export async function getInstallationObject(options: { }); } +export async function getInstallationObjects(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgNames: string[]; +}) { + const { savedObjectsClient, pkgNames } = options; + const res = await savedObjectsClient.bulkGet( + pkgNames.map((pkgName) => ({ id: pkgName, type: PACKAGES_SAVED_OBJECT_TYPE })) + ); + + return res.saved_objects.filter((so) => so?.attributes); +} + export async function getInstallation(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -339,6 +351,14 @@ export async function getInstallation(options: { return savedObject?.attributes; } +export async function getInstallationsByName(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgNames: string[]; +}) { + const savedObjects = await getInstallationObjects(options); + return savedObjects.map((so) => so.attributes); +} + function sortByName(a: { name: string }, b: { name: string }) { if (a.name > b.name) { return 1; diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 34336f8167316..8837ae3522ac1 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -60,6 +60,7 @@ describe('setupFleet', () => { (upgradeManagedPackagePolicies as jest.Mock).mockResolvedValue([]); soClient.find.mockResolvedValue({ saved_objects: [] } as any); + soClient.bulkGet.mockResolvedValue({ saved_objects: [] } as any); }); afterEach(async () => { diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 3cb2dc030cf75..e43efb44adb5f 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -12,7 +12,7 @@ import pMap from 'p-map'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; -import { AUTO_UPDATE_PACKAGES } from '../../common/constants'; +import { AUTO_UPDATE_PACKAGES, FILE_STORAGE_INTEGRATION_NAMES } from '../../common/constants'; import type { PreconfigurationError } from '../../common/constants'; import type { DefaultPackagesInstallationError, @@ -36,7 +36,10 @@ import { ensureDefaultEnrollmentAPIKeyForAgentPolicy } from './api_keys'; import { getRegistryUrl, settingsService } from '.'; import { awaitIfPending } from './setup_utils'; import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install'; -import { ensureDefaultComponentTemplates } from './epm/elasticsearch/template/install'; +import { + ensureDefaultComponentTemplates, + ensureFileUploadWriteIndices, +} from './epm/elasticsearch/template/install'; import { getInstallations, reinstallPackageForInstallation } from './epm/packages'; import { isPackageInstalled } from './epm/packages/install'; import type { UpgradeManagedPackagePoliciesResult } from './managed_package_policies'; @@ -49,6 +52,7 @@ import { ensurePreconfiguredFleetServerHosts, getPreconfiguredFleetServerHostFromConfig, } from './preconfiguration/fleet_server_host'; +import { getInstallationsByName } from './epm/packages/get'; export interface SetupStatus { isInitialized: boolean; @@ -108,6 +112,7 @@ async function createSetupSideEffects( await ensureFleetGlobalEsAssets(soClient, esClient); } + await ensureFleetFileUploadIndices(soClient, esClient); // Ensure that required packages are always installed even if they're left out of the config const preconfiguredPackageNames = new Set(packages.map((pkg) => pkg.name)); @@ -168,6 +173,30 @@ async function createSetupSideEffects( }; } +/** + * Ensure ES assets shared by all Fleet index template are installed + */ +export async function ensureFleetFileUploadIndices( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient +) { + const { diagnosticFileUploadEnabled } = appContextService.getExperimentalFeatures(); + if (!diagnosticFileUploadEnabled) return; + const logger = appContextService.getLogger(); + const installedFileUploadIntegrations = await getInstallationsByName({ + savedObjectsClient: soClient, + pkgNames: [...FILE_STORAGE_INTEGRATION_NAMES], + }); + + if (!installedFileUploadIntegrations.length) return []; + const integrationNames = installedFileUploadIntegrations.map(({ name }) => name); + logger.debug(`Ensuring file upload write indices for ${integrationNames}`); + return ensureFileUploadWriteIndices({ + esClient, + logger, + integrationNames, + }); +} /** * Ensure ES assets shared by all Fleet index template are installed */ diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx index bbd0afa495e48..4a251d41d69d9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx @@ -21,7 +21,8 @@ export const defaultScaledFloatParameters = { store: false, }; -describe('Mappings editor: scaled float datatype', () => { +// FLAKY: https://github.com/elastic/kibana/issues/145102 +describe.skip('Mappings editor: scaled float datatype', () => { /** * Variable to store the mappings data forwarded to the consumer component */ diff --git a/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts index c20a4ae39026d..a6691225808ef 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts @@ -13,6 +13,7 @@ import { NOTES_LINK, NOTES_TEXT, NOTES_TEXT_AREA, + MARKDOWN_INVESTIGATE_BUTTON, } from '../../screens/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -84,4 +85,11 @@ describe('Timeline notes tab', () => { cy.get(NOTES_LINK).last().should('have.text', `${text}(opens in a new tab or window)`); cy.get(NOTES_LINK).last().click(); }); + + it('should render insight query from markdown', () => { + addNotesToTimeline( + `!{insight{"description":"2 top level OR providers, 1 nested AND","label":"test insight", "providers": [[{ "field": "event.id", "value": "kibana.alert.original_event.id", "type": "parameter" }], [{ "field": "event.category", "value": "network", "type": "literal" }, {"field": "process.pid", "value": "process.pid", "type": "parameter"}]]}}` + ); + cy.get(MARKDOWN_INVESTIGATE_BUTTON).should('exist'); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 529847261e06d..59c8d6a4103f7 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -90,6 +90,9 @@ export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername'; export const NOTES_LINK = '[data-test-subj="markdown-link"]'; +export const MARKDOWN_INVESTIGATE_BUTTON = + '[data-test-subj="insight-investigate-in-timeline-button"]'; + export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; export const OPEN_TIMELINE_MODAL = '[data-test-subj="open-timeline-modal"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 1ff04833db2a4..76a512a6fcb88 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -161,7 +161,11 @@ export const addNotesToTimeline = (notes: string) => { .then(($el) => { const notesCount = parseInt($el.text(), 10); - cy.get(NOTES_TEXT_AREA).type(notes); + cy.get(NOTES_TEXT_AREA).type(notes, { + parseSpecialCharSequences: false, + delay: 0, + force: true, + }); cy.get(ADD_NOTE_BUTTON).trigger('click'); cy.get(`${NOTES_TAB_BUTTON} .euiBadge`).should('have.text', `${notesCount + 1}`); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 820488225e17b..3c095c5ed87da 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; import { useDispatch } from 'react-redux'; import { sourcererSelectors } from '../../../store'; import { InputsModelId } from '../../../store/inputs/constants'; +import type { TimeRange } from '../../../store/inputs/model'; import { inputsActions } from '../../../store/inputs'; import { updateProviders, setFilters } from '../../../../timelines/store/timeline/actions'; import { sourcererActions } from '../../../store/actions'; @@ -26,7 +27,10 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ asEmptyButton: boolean; dataProviders: DataProvider[] | null; filters?: Filter[] | null; -}> = ({ asEmptyButton, children, dataProviders, filters, ...rest }) => { + timeRange?: TimeRange; + keepDataView?: boolean; + isDisabled?: boolean; +}> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { const dispatch = useDispatch(); const getDataViewsSelector = useMemo( @@ -37,15 +41,24 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ getDataViewsSelector(state) ); + const hasTemplateProviders = + dataProviders && dataProviders.find((provider) => provider.type === 'template'); + const clearTimeline = useCreateTimeline({ timelineId: TimelineId.active, - timelineType: TimelineType.default, + timelineType: hasTemplateProviders ? TimelineType.template : TimelineType.default, }); - const configureAndOpenTimeline = React.useCallback(() => { + const configureAndOpenTimeline = useCallback(() => { if (dataProviders || filters) { // Reset the current timeline - clearTimeline(); + if (timeRange) { + clearTimeline({ + timeRange, + }); + } else { + clearTimeline(); + } if (dataProviders) { // Update the timeline's providers to match the current prevalence field query dispatch( @@ -66,17 +79,28 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ } // Only show detection alerts // (This is required so the timeline event count matches the prevalence count) - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: defaultDataView.id, - selectedPatterns: [signalIndexName || ''], - }) - ); + if (!keepDataView) { + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: defaultDataView.id, + selectedPatterns: [signalIndexName || ''], + }) + ); + } // Unlock the time range from the global time range dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); } - }, [dataProviders, clearTimeline, dispatch, defaultDataView.id, signalIndexName, filters]); + }, [ + dataProviders, + clearTimeline, + dispatch, + defaultDataView.id, + signalIndexName, + filters, + timeRange, + keepDataView, + ]); return asEmptyButton ? ( { + return value.indexOf(insightPrefix, fromIndex); + }; + tokenizers.insight = tokenizeInsight; + methods.splice(methods.indexOf('text'), 0, 'insight'); +}; + +// receives the configuration from the parser and renders +const InsightComponent = ({ label, description, providers }: InsightComponentProps) => { + const { addError } = useAppToasts(); + let parsedProviders = []; + try { + if (providers !== undefined) { + parsedProviders = JSON.parse(providers); + } + } catch (err) { + addError(err, { + title: i18n.translate('xpack.securitySolution.markdownEditor.plugins.insightProviderError', { + defaultMessage: 'Unable to parse insight provider configuration', + }), + }); + } + const { data: alertData } = useContext(BasicAlertDataContext); + const dataProviders = useInsightDataProviders({ + providers: parsedProviders, + alertData, + }); + const { totalCount, isQueryLoading, oldestTimestamp, hasError } = useInsightQuery({ + dataProviders, + }); + const timerange: AbsoluteTimeRange = useMemo(() => { + return { + kind: 'absolute', + from: oldestTimestamp ?? '', + to: new Date().toISOString(), + }; + }, [oldestTimestamp]); + if (isQueryLoading) { + return ; + } else { + return ( + + + {` ${label} (${totalCount}) - ${description}`} + + ); + } +}; + +export { InsightComponent as renderer }; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts new file mode 100644 index 0000000000000..8542c445b5d14 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import type { DataProvider } from '@kbn/timelines-plugin/common'; +import type { UseInsightDataProvidersProps, Provider } from './use_insight_data_providers'; +import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; +import { useInsightDataProviders } from './use_insight_data_providers'; +import { mockAlertDetailsData } from '../../../event_details/__mocks__'; + +const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => { + return { + ...detail, + isObjectArray: false, + }; +}) as TimelineEventsDetailsItem[]; + +const nestedAndProvider = [ + [ + { + field: 'event.id', + value: 'kibana.alert.rule.uuid', + type: 'parameter', + }, + ], + [ + { + field: 'event.category', + value: 'network', + type: 'literal', + }, + { + field: 'process.pid', + value: 'process.pid', + type: 'parameter', + }, + ], +] as Provider[][]; + +const topLevelOnly = [ + [ + { + field: 'event.id', + value: 'kibana.alert.rule.uuid', + type: 'parameter', + }, + ], + [ + { + field: 'event.category', + value: 'network', + type: 'literal', + }, + ], + [ + { + field: 'process.pid', + value: 'process.pid', + type: 'parameter', + }, + ], +] as Provider[][]; + +const nonExistantField = [ + [ + { + field: 'event.id', + value: 'kibana.alert.rule.parameters.threshold.field', + type: 'parameter', + }, + ], +] as Provider[][]; + +describe('useInsightDataProviders', () => { + it('should return 2 data providers, 1 with a nested provider ANDed to it', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: nestedAndProvider, + alertData: mockAlertDetailsDataWithIsObject, + }) + ); + const providers = result.current; + const providersWithNonEmptyAnd = providers.filter((provider) => provider.and.length > 0); + expect(providers.length).toBe(2); + expect(providersWithNonEmptyAnd.length).toBe(1); + }); + + it('should return 3 data providers without any containing nested ANDs', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: topLevelOnly, + alertData: mockAlertDetailsDataWithIsObject, + }) + ); + const providers = result.current; + const providersWithNonEmptyAnd = providers.filter((provider) => provider.and.length > 0); + expect(providers.length).toBe(3); + expect(providersWithNonEmptyAnd.length).toBe(0); + }); + + it('should use a wildcard for a field not present in an alert', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: nonExistantField, + alertData: mockAlertDetailsDataWithIsObject, + }) + ); + const providers = result.current; + const { + queryMatch: { value }, + } = providers[0]; + expect(providers.length).toBe(1); + expect(value).toBe('*'); + }); + + it('should use template data providers when called without alertData', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: nestedAndProvider, + }) + ); + const providers = result.current; + const [first, second] = providers; + const [nestedSecond] = second.and; + expect(second.type).toBe('default'); + expect(first.type).toBe('template'); + expect(nestedSecond.type).toBe('template'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts new file mode 100644 index 0000000000000..5c5de496b04b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts @@ -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 { useMemo } from 'react'; +import type { QueryOperator, DataProvider } from '@kbn/timelines-plugin/common'; +import { DataProviderType } from '@kbn/timelines-plugin/common'; +import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; +import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; + +export interface Provider { + field: string; + value: string; + type: 'parameter' | 'value'; +} +export interface UseInsightDataProvidersProps { + providers: Provider[][]; + alertData?: TimelineEventsDetailsItem[] | null; +} + +export const useInsightDataProviders = ({ + providers, + alertData, +}: UseInsightDataProvidersProps): DataProvider[] => { + function getFieldValue(fields: TimelineEventsDetailsItem[], fieldToFind: string) { + const alertField = fields.find((dataField) => dataField.field === fieldToFind); + return alertField?.values ? alertField.values[0] : '*'; + } + const dataProviders: DataProvider[] = useMemo(() => { + if (alertData) { + return providers.map((innerProvider) => { + return innerProvider.reduce((prev, next, index): DataProvider => { + const { field, value, type } = next; + if (index === 0) { + return { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? getFieldValue(alertData, value) : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + } else { + const newProvider = { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? getFieldValue(alertData, value) : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + prev.and.push(newProvider); + } + return prev; + }, {} as DataProvider); + }); + } else { + return providers.map((innerProvider) => { + return innerProvider.reduce((prev, next, index) => { + const { field, value, type } = next; + if (index === 0) { + return { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: type === 'parameter' ? DataProviderType.template : DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? `{${value}}` : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + } else { + const newProvider = { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: type === 'parameter' ? DataProviderType.template : DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? `{${value}}` : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + prev.and.push(newProvider); + } + return prev; + }, {} as DataProvider); + }); + } + }, [alertData, providers]); + return dataProviders; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts new file mode 100644 index 0000000000000..74942f0f4ad38 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import type { QueryOperator } from '@kbn/timelines-plugin/common'; +import { DataProviderType } from '@kbn/timelines-plugin/common'; +import { useInsightQuery } from './use_insight_query'; +import { TestProviders } from '../../../../mock'; +import type { UseInsightQuery, UseInsightQueryResult } from './use_insight_query'; +import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; + +const mockProvider = { + and: [], + enabled: true, + id: 'made-up-id', + name: 'test', + excluded: false, + kqlQuery: '', + type: DataProviderType.default, + queryMatch: { + field: 'event.id', + value: '*', + operator: IS_OPERATOR as QueryOperator, + }, +}; + +describe('useInsightQuery', () => { + it('should return renderable defaults', () => { + const { result } = renderHook( + () => + useInsightQuery({ + dataProviders: [mockProvider], + }), + { + wrapper: TestProviders, + } + ); + const { isQueryLoading, totalCount, oldestTimestamp } = result.current; + expect(isQueryLoading).toBeFalsy(); + expect(totalCount).toBe(-1); + expect(oldestTimestamp).toBe(undefined); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts new file mode 100644 index 0000000000000..e7836cd6cd3ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo, useState } from 'react'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import type { DataProvider } from '@kbn/timelines-plugin/common'; +import { TimelineId } from '../../../../../../common/types/timeline'; +import { useKibana } from '../../../../lib/kibana'; +import { combineQueries } from '../../../../lib/kuery'; +import { useTimelineEvents } from '../../../../../timelines/containers'; +import { useSourcererDataView } from '../../../../containers/sourcerer'; +import { SourcererScopeName } from '../../../../store/sourcerer/model'; + +export interface UseInsightQuery { + dataProviders: DataProvider[]; +} + +export interface UseInsightQueryResult { + isQueryLoading: boolean; + totalCount: number; + oldestTimestamp: string | null | undefined; + hasError: boolean; +} + +export const useInsightQuery = ({ dataProviders }: UseInsightQuery): UseInsightQueryResult => { + const { uiSettings } = useKibana().services; + const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); + const { browserFields, selectedPatterns, indexPattern, dataViewId } = useSourcererDataView( + SourcererScopeName.timeline + ); + const [hasError, setHasError] = useState(false); + const combinedQueries = useMemo(() => { + try { + if (hasError === false) { + const parsedCombinedQueries = combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: [], + kqlQuery: { + query: '', + language: 'kuery', + }, + kqlMode: 'filter', + }); + return parsedCombinedQueries; + } + } catch (err) { + setHasError(true); + return null; + } + }, [browserFields, dataProviders, esQueryConfig, hasError, indexPattern]); + + const [isQueryLoading, { events, totalCount }] = useTimelineEvents({ + dataViewId, + fields: ['*'], + filterQuery: combinedQueries?.filterQuery, + id: TimelineId.active, + indexNames: selectedPatterns, + language: 'kuery', + limit: 1, + runtimeMappings: {}, + }); + const [oldestEvent] = events; + const timestamp = + oldestEvent && oldestEvent.data && oldestEvent.data.find((d) => d.field === '@timestamp'); + const oldestTimestamp = timestamp && timestamp.value && timestamp.value[0]; + return { + isQueryLoading, + totalCount, + oldestTimestamp, + hasError, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx index 3d046e349de31..d65405cd1c48f 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx @@ -258,6 +258,7 @@ const RunOsqueryButtonRenderer = ({ label?: string; query: string; ecs_mapping: { [key: string]: {} }; + test: []; }; }) => { const [showFlyout, setShowFlyout] = useState(false); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx index 6dcb93321056e..449e2adee8bf8 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx @@ -24,7 +24,6 @@ const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) () => (props) => , [disableLinks] ); - // Deep clone of the processing plugins to prevent affecting the markdown editor. const processingPluginList = cloneDeep(processingPlugins); // This line of code is TS-compatible and it will break if [1][1] change in the future. diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx index 6bee089027477..f91a9eed0d165 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx @@ -19,6 +19,7 @@ export interface GetBasicDataFromDetailsData { userName: string; ruleName: string; timestamp: string; + data: TimelineEventsDetailsItem[] | null; } export const useBasicDataFromDetailsData = ( @@ -62,8 +63,9 @@ export const useBasicDataFromDetailsData = ( userName, ruleName, timestamp, + data, }), - [agentId, alertId, hostName, isAlert, ruleName, timestamp, userName] + [agentId, alertId, hostName, isAlert, ruleName, timestamp, userName, data] ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 5324c02fcbfdf..55c8d2cad65bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -81,7 +81,7 @@ type TimelineResponse = T extends 'kuery' export interface UseTimelineEventsProps { dataViewId: string | null; - endDate: string; + endDate?: string; eqlOptions?: EqlOptionsSelected; fields: string[]; filterQuery?: ESQuery | string; @@ -92,7 +92,7 @@ export interface UseTimelineEventsProps { runtimeMappings: MappingRuntimeFields; skip?: boolean; sort?: TimelineRequestSortField[]; - startDate: string; + startDate?: string; timerangeKind?: 'absolute' | 'relative'; } @@ -360,17 +360,17 @@ export const useTimelineEventsHandler = ({ ...deStructureEqlOptions(prevEqlRequest), }; + const timerange = + startDate && endDate + ? { timerange: { interval: '12h', from: startDate, to: endDate } } + : {}; const currentSearchParameters = { defaultIndex: indexNames, filterQuery: createFilter(filterQuery), querySize: limit, sort, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, runtimeMappings, + ...timerange, ...deStructureEqlOptions(eqlOptions), }; @@ -391,11 +391,7 @@ export const useTimelineEventsHandler = ({ language, runtimeMappings, sort, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, + ...timerange, ...(eqlOptions ? eqlOptions : {}), }; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index e0b449a6afa8b..409f29bfc437b 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4071,6 +4071,72 @@ }, "indices": { "properties": { + "metric": { + "properties": { + "shards": { + "properties": { + "total": { + "type": "long" + } + } + }, + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "traces": { + "properties": { + "shards": { + "properties": { + "total": { + "type": "long" + } + } + }, + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + } + } + }, "shards": { "properties": { "total": { @@ -4122,6 +4188,12 @@ "service_id": { "type": "keyword" }, + "num_service_nodes": { + "type": "long" + }, + "num_transaction_types": { + "type": "long" + }, "timed_out": { "type": "boolean" }, @@ -13015,7 +13087,9 @@ "properties": { "unique_endpoint_count": { "type": "long", - "_meta": { "description": "Number of active unique endpoints in last 24 hours" } + "_meta": { + "description": "Number of active unique endpoints in last 24 hours" + } } } } diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts index dfdc1ed3eabd4..a5856169a5748 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts @@ -32,7 +32,7 @@ export * from './events'; export type TimelineFactoryQueryTypes = TimelineEventsQueries; export interface TimelineRequestBasicOptions extends IEsSearchRequest { - timerange: TimerangeInput; + timerange?: TimerangeInput; filterQuery: ESQuery | string | undefined; defaultIndex: string[]; factoryQueryType?: TimelineFactoryQueryTypes; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts index 5424568c44a70..e9a2ef7e49cda 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts @@ -27,17 +27,19 @@ export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record = { // eslint-disable-next-line prefer-const let { fieldRequested, ...queryOptions } = cloneDeep(options); queryOptions.fields = buildFieldsRequest(fieldRequested, queryOptions.excludeEcsData); + const { activePage, querySize } = options.pagination; const producerBuckets = getOr([], 'aggregations.producers.buckets', response.rawResponse); const totalCount = response.rawResponse.hits.total || 0; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index 9f6902c65f639..5ae88bcf6f460 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -28,7 +28,6 @@ export const buildTimelineEventsAllQuery = ({ timerange, }: Omit) => { const filterClause = [...createQueryFilterClauses(filterQuery)]; - const getTimerangeFilter = (timerangeOption: TimerangeInput | undefined): TimerangeFilter[] => { if (timerangeOption) { const { to, from } = timerangeOption; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 32e7d696d8dc3..2a006ccb2cc8b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -10348,13 +10348,59 @@ "xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "Métadonnées pour le document : {id}", "xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "Voulez-vous vraiment supprimer le domaine \"{domainUrl}\" et tous ses paramètres ?", "xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "Le point d'entrée du robot d'indexation a été défini sur {entryPointValue}", - "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.formDescription": "Configurer l'indexation automatisée. {readMoreMessage}.", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlCountOnDomains": "{crawlType} indexation sur {domainCount, plural, one {# domaine} other {# domaines}}", "xpack.enterpriseSearch.crawler.crawlCustomSettingsFlyout.includeSitemapsCheckboxLabel": "Inclure les plans de site découverts dans {robotsDotTxt}", "xpack.enterpriseSearch.crawler.crawlRulesTable.description": "Créez une règle d'indexation pour inclure ou exclure les pages dont l'URL correspond à la règle. Les règles sont exécutées dans l'ordre séquentiel, et chaque URL est évaluée en fonction de la première correspondance. {link}", "xpack.enterpriseSearch.crawler.deduplicationPanel.description": "Le robot d'indexation n'indexe que les pages uniques. Choisissez les champs que le robot d'indexation doit utiliser lorsqu'il recherche les pages en double. Désélectionnez tous les champs de schéma pour autoriser les documents en double dans ce domaine. {documentationLink}.", "xpack.enterpriseSearch.crawler.deleteDomainModal.description": "Supprimer le domaine {domainUrl} de votre robot d'indexation. Cela supprimera également tous les points d'entrée et toutes les règles d'indexation que vous avez configurés. Tous les documents associés à ce domaine seront supprimés lors de la prochaine indexation. {thisCannotBeUndoneMessage}", "xpack.enterpriseSearch.crawler.entryPointsTable.emptyMessageDescription": "{link} pour spécifier un point d'entrée pour le robot d'indexation", + "xpack.enterpriseSearch.cronEditor.cronDaily.fieldHour.textAtLabel": "À", + "xpack.enterpriseSearch.cronEditor.cronDaily.fieldTimeLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronDaily.hourSelectLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronDaily.minuteSelectLabel": "Minute", + "xpack.enterpriseSearch.cronEditor.cronHourly.fieldMinute.textAtLabel": "À", + "xpack.enterpriseSearch.cronEditor.cronHourly.fieldTimeLabel": "Minute", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldDateLabel": "Date", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldHour.textAtLabel": "À", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldTimeLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronMonthly.hourSelectLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronMonthly.minuteSelectLabel": "Minute", + "xpack.enterpriseSearch.cronEditor.cronMonthly.textOnTheLabel": "Le", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldDateLabel": "Jour", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldHour.textAtLabel": "À", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldTimeLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronWeekly.hourSelectLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronWeekly.minuteSelectLabel": "Minute", + "xpack.enterpriseSearch.cronEditor.cronWeekly.textOnLabel": "Le", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldDate.textOnTheLabel": "Le", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldDateLabel": "Date", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldHour.textAtLabel": "À", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldMonth.textInLabel": "En", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldMonthLabel": "Mois", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldTimeLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronYearly.hourSelectLabel": "Heure", + "xpack.enterpriseSearch.cronEditor.cronYearly.minuteSelectLabel": "Minute", + "xpack.enterpriseSearch.cronEditor.day.friday": "vendredi", + "xpack.enterpriseSearch.cronEditor.day.monday": "lundi", + "xpack.enterpriseSearch.cronEditor.day.saturday": "samedi", + "xpack.enterpriseSearch.cronEditor.day.sunday": "dimanche", + "xpack.enterpriseSearch.cronEditor.day.thursday": "jeudi", + "xpack.enterpriseSearch.cronEditor.day.tuesday": "mardi", + "xpack.enterpriseSearch.cronEditor.day.wednesday": "mercredi", + "xpack.enterpriseSearch.cronEditor.fieldFrequencyLabel": "Fréquence", + "xpack.enterpriseSearch.cronEditor.month.april": "avril", + "xpack.enterpriseSearch.cronEditor.month.august": "août", + "xpack.enterpriseSearch.cronEditor.month.december": "décembre", + "xpack.enterpriseSearch.cronEditor.month.february": "février", + "xpack.enterpriseSearch.cronEditor.month.january": "janvier", + "xpack.enterpriseSearch.cronEditor.month.july": "juillet", + "xpack.enterpriseSearch.cronEditor.month.june": "juin", + "xpack.enterpriseSearch.cronEditor.month.march": "mars", + "xpack.enterpriseSearch.cronEditor.month.may": "mai", + "xpack.enterpriseSearch.cronEditor.month.november": "novembre", + "xpack.enterpriseSearch.cronEditor.month.october": "octobre", + "xpack.enterpriseSearch.cronEditor.month.september": "septembre", + "xpack.enterpriseSearch.cronEditor.textEveryLabel": "Chaque", "xpack.enterpriseSearch.errorConnectingState.cloudErrorMessage": "Les nœuds Enterprise Search fonctionnent-ils dans votre déploiement cloud ? {deploymentSettingsLink}", "xpack.enterpriseSearch.errorConnectingState.description1": "Impossible d'établir une connexion à Enterprise Search avec l'URL hôte {enterpriseSearchUrl} en raison de l'erreur suivante :", "xpack.enterpriseSearch.errorConnectingState.description2": "Vérifiez que l'URL hôte est correctement configurée dans {configFile}.", @@ -11386,21 +11432,21 @@ "xpack.enterpriseSearch.content.newIndex.steps.configureIngestion.title": "Configurer les paramètres d’ingestion", "xpack.enterpriseSearch.content.newIndex.steps.createIndex.crawler.title": "Indexer avec le robot d'indexation", "xpack.enterpriseSearch.content.newIndex.steps.createIndex.title": "Créer un index Elasticsearch", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.chineseDropDownOptionLabel": "Chinois", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.danishDropDownOptionLabel": "Danois", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.dutchDropDownOptionLabel": "Néerlandais", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.englishDropDownOptionLabel": "Anglais", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.frenchDropDownOptionLabel": "Français", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.germanDropDownOptionLabel": "Allemand", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.italianDropDownOptionLabel": "Italien", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.japaneseDropDownOptionLabel": "Japonais", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.koreanDropDownOptionLabel": "Coréen", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseBrazilDropDownOptionLabel": "Portugais (Brésil)", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseDropDownOptionLabel": "Portugais", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.russianDropDownOptionLabel": "Russe", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.spanishDropDownOptionLabel": "Espagnol", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.thaiDropDownOptionLabel": "Thaï", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.universalDropDownOptionLabel": "Universel", + "xpack.enterpriseSearch.content.supportedLanguages.chineseLabel": "Chinois", + "xpack.enterpriseSearch.content.supportedLanguages.danishLabel": "Danois", + "xpack.enterpriseSearch.content.supportedLanguages.dutchLabel": "Néerlandais", + "xpack.enterpriseSearch.content.supportedLanguages.englishLabel": "Anglais", + "xpack.enterpriseSearch.content.supportedLanguages.frenchLabel": "Français", + "xpack.enterpriseSearch.content.supportedLanguages.germanLabel": "Allemand", + "xpack.enterpriseSearch.content.supportedLanguages.italianLabel": "Italien", + "xpack.enterpriseSearch.content.supportedLanguages.japaneseLabel": "Japonais", + "xpack.enterpriseSearch.content.supportedLanguages.koreanLabel": "Coréen", + "xpack.enterpriseSearch.content.supportedLanguages.portugueseBrazilLabel": "Portugais (Brésil)", + "xpack.enterpriseSearch.content.supportedLanguages.portugueseLabel": "Portugais", + "xpack.enterpriseSearch.content.supportedLanguages.russianLabel": "Russe", + "xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "Espagnol", + "xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "Thaï", + "xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "Universel", "xpack.enterpriseSearch.content.newIndex.types.api": "Point de terminaison d'API", "xpack.enterpriseSearch.content.newIndex.types.connector": "Connecteur", "xpack.enterpriseSearch.content.newIndex.types.crawler": "Robot d'indexation", @@ -11490,13 +11536,10 @@ "xpack.enterpriseSearch.crawler.addDomainForm.urlLabel": "URL de domaine", "xpack.enterpriseSearch.crawler.addDomainForm.validateButtonLabel": "Valider le domaine", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlAutomaticallySwitchLabel": "Indexer automatiquement", - "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlUnitsPrefix": "Chaque", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.readMoreLink": "En lire plus.", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleDescription": "Le calendrier d’indexation effectuera une indexation complète de chaque domaine de cet index.", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleFrequencyLabel": "Planifier la fréquence", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleUnitsLabel": "Planifier des unités de temps", - "xpack.enterpriseSearch.crawler.automaticCrawlScheduler.disableCrawlSchedule.successMessage": "L'indexation automatique a été désactivée.", - "xpack.enterpriseSearch.crawler.automaticCrawlScheduler.submitCrawlSchedule.successMessage": "Votre planification d'indexation automatique a été mise à jour.", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlDepthLabel": "Profondeur maximale de l'indexation", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlTypeLabel": "Type d'indexation", "xpack.enterpriseSearch.crawler.crawlCustomSettingsFlyout.customEntryPointUrlsTextboxLabel": "URL de points d'entrée personnalisés", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bcec0383dcb33..a105bd0aa0d8f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10334,13 +10334,59 @@ "xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "ドキュメント{id}のメタデータ", "xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "ドメイン\"{domainUrl}\"とすべての設定を削除しますか?", "xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "Webクローラーエントリポイントが{entryPointValue}として設定されました", - "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.formDescription": "自動クロールを設定します。{readMoreMessage}。", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlCountOnDomains": "{domainCount, plural, other {# 件のドメイン}}で{crawlType}クロール", "xpack.enterpriseSearch.crawler.crawlCustomSettingsFlyout.includeSitemapsCheckboxLabel": "{robotsDotTxt}で検出されたサイトマップを含める", "xpack.enterpriseSearch.crawler.crawlRulesTable.description": "URLがルールと一致するページを含めるか除外するためのクロールルールを作成します。ルールは連続で実行されます。各URLは最初の一致に従って評価されます。{link}", "xpack.enterpriseSearch.crawler.deduplicationPanel.description": "Webクローラーは一意のページにのみインデックスします。重複するページを検討するときにクローラーが使用するフィールドを選択します。すべてのスキーマフィールドを選択解除して、このドメインで重複するドキュメントを許可します。{documentationLink}。", "xpack.enterpriseSearch.crawler.deleteDomainModal.description": "ドメイン{domainUrl}をクローラーから削除します。これにより、設定したすべてのエントリポイントとクロールルールも削除されます。このドメインに関連するすべてのドキュメントは、次回のクロールで削除されます。{thisCannotBeUndoneMessage}", "xpack.enterpriseSearch.crawler.entryPointsTable.emptyMessageDescription": "クローラーのエントリポイントを指定するには、{link}してください", + "xpack.enterpriseSearch.cronEditor.cronDaily.fieldHour.textAtLabel": "に", + "xpack.enterpriseSearch.cronEditor.cronDaily.fieldTimeLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronDaily.hourSelectLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronDaily.minuteSelectLabel": "分", + "xpack.enterpriseSearch.cronEditor.cronHourly.fieldMinute.textAtLabel": "に", + "xpack.enterpriseSearch.cronEditor.cronHourly.fieldTimeLabel": "分", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldDateLabel": "日付", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldHour.textAtLabel": "に", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldTimeLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronMonthly.hourSelectLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronMonthly.minuteSelectLabel": "分", + "xpack.enterpriseSearch.cronEditor.cronMonthly.textOnTheLabel": "に", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldDateLabel": "日", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldHour.textAtLabel": "に", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldTimeLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronWeekly.hourSelectLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronWeekly.minuteSelectLabel": "分", + "xpack.enterpriseSearch.cronEditor.cronWeekly.textOnLabel": "オン", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldDate.textOnTheLabel": "に", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldDateLabel": "日付", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldHour.textAtLabel": "に", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldMonth.textInLabel": "入", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldMonthLabel": "月", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldTimeLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronYearly.hourSelectLabel": "時間", + "xpack.enterpriseSearch.cronEditor.cronYearly.minuteSelectLabel": "分", + "xpack.enterpriseSearch.cronEditor.day.friday": "金曜日", + "xpack.enterpriseSearch.cronEditor.day.monday": "月曜日", + "xpack.enterpriseSearch.cronEditor.day.saturday": "土曜日", + "xpack.enterpriseSearch.cronEditor.day.sunday": "日曜日", + "xpack.enterpriseSearch.cronEditor.day.thursday": "木曜日", + "xpack.enterpriseSearch.cronEditor.day.tuesday": "火曜日", + "xpack.enterpriseSearch.cronEditor.day.wednesday": "水曜日", + "xpack.enterpriseSearch.cronEditor.fieldFrequencyLabel": "頻度", + "xpack.enterpriseSearch.cronEditor.month.april": "4 月", + "xpack.enterpriseSearch.cronEditor.month.august": "8 月", + "xpack.enterpriseSearch.cronEditor.month.december": "12 月", + "xpack.enterpriseSearch.cronEditor.month.february": "2 月", + "xpack.enterpriseSearch.cronEditor.month.january": "1 月", + "xpack.enterpriseSearch.cronEditor.month.july": "7 月", + "xpack.enterpriseSearch.cronEditor.month.june": "6 月", + "xpack.enterpriseSearch.cronEditor.month.march": "3 月", + "xpack.enterpriseSearch.cronEditor.month.may": "5月", + "xpack.enterpriseSearch.cronEditor.month.november": "11 月", + "xpack.enterpriseSearch.cronEditor.month.october": "10 月", + "xpack.enterpriseSearch.cronEditor.month.september": "9 月", + "xpack.enterpriseSearch.cronEditor.textEveryLabel": "毎", "xpack.enterpriseSearch.errorConnectingState.cloudErrorMessage": "クラウドデプロイのエンタープライズ サーチノードが実行中ですか?{deploymentSettingsLink}", "xpack.enterpriseSearch.errorConnectingState.description1": "次のエラーのため、ホストURL {enterpriseSearchUrl}では、エンタープライズ サーチへの接続を確立できません。", "xpack.enterpriseSearch.errorConnectingState.description2": "ホストURLが{configFile}で正しく構成されていることを確認してください。", @@ -11372,21 +11418,21 @@ "xpack.enterpriseSearch.content.newIndex.steps.configureIngestion.title": "インジェスチョン設定を構成", "xpack.enterpriseSearch.content.newIndex.steps.createIndex.crawler.title": "Webクローラーを使用してインデックス", "xpack.enterpriseSearch.content.newIndex.steps.createIndex.title": "Elasticsearchインデックスを作成", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.chineseDropDownOptionLabel": "中国語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.danishDropDownOptionLabel": "デンマーク語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.dutchDropDownOptionLabel": "オランダ語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.englishDropDownOptionLabel": "英語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.frenchDropDownOptionLabel": "フランス語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.germanDropDownOptionLabel": "ドイツ語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.italianDropDownOptionLabel": "イタリア語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.japaneseDropDownOptionLabel": "日本語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.koreanDropDownOptionLabel": "韓国語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseBrazilDropDownOptionLabel": "ポルトガル語(ブラジル)", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseDropDownOptionLabel": "ポルトガル語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.russianDropDownOptionLabel": "ロシア語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.spanishDropDownOptionLabel": "スペイン語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.thaiDropDownOptionLabel": "タイ語", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.universalDropDownOptionLabel": "ユニバーサル", + "xpack.enterpriseSearch.content.supportedLanguages.chineseLabel": "中国語", + "xpack.enterpriseSearch.content.supportedLanguages.danishLabel": "デンマーク語", + "xpack.enterpriseSearch.content.supportedLanguages.dutchLabel": "オランダ語", + "xpack.enterpriseSearch.content.supportedLanguages.englishLabel": "英語", + "xpack.enterpriseSearch.content.supportedLanguages.frenchLabel": "フランス語", + "xpack.enterpriseSearch.content.supportedLanguages.germanLabel": "ドイツ語", + "xpack.enterpriseSearch.content.supportedLanguages.italianLabel": "イタリア語", + "xpack.enterpriseSearch.content.supportedLanguages.japaneseLabel": "日本語", + "xpack.enterpriseSearch.content.supportedLanguages.koreanLabel": "韓国語", + "xpack.enterpriseSearch.content.supportedLanguages.portugueseBrazilLabel": "ポルトガル語(ブラジル)", + "xpack.enterpriseSearch.content.supportedLanguages.portugueseLabel": "ポルトガル語", + "xpack.enterpriseSearch.content.supportedLanguages.russianLabel": "ロシア語", + "xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "スペイン語", + "xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "タイ語", + "xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "ユニバーサル", "xpack.enterpriseSearch.content.newIndex.types.api": "APIエンドポイント", "xpack.enterpriseSearch.content.newIndex.types.connector": "コネクター", "xpack.enterpriseSearch.content.newIndex.types.crawler": "Webクローラー", @@ -11476,13 +11522,10 @@ "xpack.enterpriseSearch.crawler.addDomainForm.urlLabel": "ドメインURL", "xpack.enterpriseSearch.crawler.addDomainForm.validateButtonLabel": "ドメインを検証", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlAutomaticallySwitchLabel": "自動的にクロール", - "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlUnitsPrefix": "毎", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.readMoreLink": "詳細をお読みください。", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleDescription": "クローリングスケジュールは、このインデックスのすべてのドメインに対してフルクローリングを実行します。", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleFrequencyLabel": "スケジュール頻度", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleUnitsLabel": "スケジュール時間単位", - "xpack.enterpriseSearch.crawler.automaticCrawlScheduler.disableCrawlSchedule.successMessage": "自動クローリングが無効にされました。", - "xpack.enterpriseSearch.crawler.automaticCrawlScheduler.submitCrawlSchedule.successMessage": "自動クローリングスケジュールが更新されました。", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlDepthLabel": "最大クロール深度", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlTypeLabel": "クロールタイプ", "xpack.enterpriseSearch.crawler.crawlCustomSettingsFlyout.customEntryPointUrlsTextboxLabel": "カスタム入力ポイントURL", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0aa3a6f587043..de988524c860b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10353,13 +10353,59 @@ "xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "以下文档的元数据:{id}", "xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "确定要移除域“{domainUrl}”和其所有设置?", "xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "网络爬虫入口点已设置为 {entryPointValue}", - "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.formDescription": "设置自动爬网。{readMoreMessage}。", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlCountOnDomains": "在 {domainCount, plural, other {# 个域}}上进行 {crawlType} 爬网", "xpack.enterpriseSearch.crawler.crawlCustomSettingsFlyout.includeSitemapsCheckboxLabel": "包括在 {robotsDotTxt} 中发现的站点地图", "xpack.enterpriseSearch.crawler.crawlRulesTable.description": "创建爬网规则以包括或排除 URL 匹配规则的页面。规则按顺序运行,每个 URL 根据第一个匹配进行评估。{link}", "xpack.enterpriseSearch.crawler.deduplicationPanel.description": "网络爬虫仅索引唯一的页面。选择网络爬虫在考虑哪些网页重复时应使用的字段。取消选择所有架构字段以在此域上允许重复的文档。{documentationLink}。", "xpack.enterpriseSearch.crawler.deleteDomainModal.description": "从网络爬虫中移除域 {domainUrl}。这还会删除您已设置的所有入口点和爬网规则。将在下次爬网时移除与此域相关的任何文档。{thisCannotBeUndoneMessage}", "xpack.enterpriseSearch.crawler.entryPointsTable.emptyMessageDescription": "{link}以指定网络爬虫的入口点", + "xpack.enterpriseSearch.cronEditor.cronDaily.fieldHour.textAtLabel": "于", + "xpack.enterpriseSearch.cronEditor.cronDaily.fieldTimeLabel": "时间", + "xpack.enterpriseSearch.cronEditor.cronDaily.hourSelectLabel": "小时", + "xpack.enterpriseSearch.cronEditor.cronDaily.minuteSelectLabel": "分钟", + "xpack.enterpriseSearch.cronEditor.cronHourly.fieldMinute.textAtLabel": "@ 符号", + "xpack.enterpriseSearch.cronEditor.cronHourly.fieldTimeLabel": "分钟", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldDateLabel": "日期", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldHour.textAtLabel": "@ 符号", + "xpack.enterpriseSearch.cronEditor.cronMonthly.fieldTimeLabel": "时间", + "xpack.enterpriseSearch.cronEditor.cronMonthly.hourSelectLabel": "小时", + "xpack.enterpriseSearch.cronEditor.cronMonthly.minuteSelectLabel": "分钟", + "xpack.enterpriseSearch.cronEditor.cronMonthly.textOnTheLabel": "在", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldDateLabel": "天", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldHour.textAtLabel": "@ 符号", + "xpack.enterpriseSearch.cronEditor.cronWeekly.fieldTimeLabel": "时间", + "xpack.enterpriseSearch.cronEditor.cronWeekly.hourSelectLabel": "小时", + "xpack.enterpriseSearch.cronEditor.cronWeekly.minuteSelectLabel": "分钟", + "xpack.enterpriseSearch.cronEditor.cronWeekly.textOnLabel": "开启", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldDate.textOnTheLabel": "在", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldDateLabel": "日期", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldHour.textAtLabel": "@ 符号", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldMonth.textInLabel": "传入", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldMonthLabel": "月", + "xpack.enterpriseSearch.cronEditor.cronYearly.fieldTimeLabel": "时间", + "xpack.enterpriseSearch.cronEditor.cronYearly.hourSelectLabel": "小时", + "xpack.enterpriseSearch.cronEditor.cronYearly.minuteSelectLabel": "分钟", + "xpack.enterpriseSearch.cronEditor.day.friday": "星期五", + "xpack.enterpriseSearch.cronEditor.day.monday": "星期一", + "xpack.enterpriseSearch.cronEditor.day.saturday": "星期六", + "xpack.enterpriseSearch.cronEditor.day.sunday": "星期日", + "xpack.enterpriseSearch.cronEditor.day.thursday": "星期四", + "xpack.enterpriseSearch.cronEditor.day.tuesday": "星期二", + "xpack.enterpriseSearch.cronEditor.day.wednesday": "星期三", + "xpack.enterpriseSearch.cronEditor.fieldFrequencyLabel": "频率", + "xpack.enterpriseSearch.cronEditor.month.april": "四月", + "xpack.enterpriseSearch.cronEditor.month.august": "八月", + "xpack.enterpriseSearch.cronEditor.month.december": "十二月", + "xpack.enterpriseSearch.cronEditor.month.february": "二月", + "xpack.enterpriseSearch.cronEditor.month.january": "一月", + "xpack.enterpriseSearch.cronEditor.month.july": "七月", + "xpack.enterpriseSearch.cronEditor.month.june": "六月", + "xpack.enterpriseSearch.cronEditor.month.march": "三月", + "xpack.enterpriseSearch.cronEditor.month.may": "五月", + "xpack.enterpriseSearch.cronEditor.month.november": "十一月", + "xpack.enterpriseSearch.cronEditor.month.october": "十月", + "xpack.enterpriseSearch.cronEditor.month.september": "九月", + "xpack.enterpriseSearch.cronEditor.textEveryLabel": "每", "xpack.enterpriseSearch.errorConnectingState.cloudErrorMessage": "您的云部署是否正在运行 Enterprise Search 节点?{deploymentSettingsLink}", "xpack.enterpriseSearch.errorConnectingState.description1": "由于以下错误,我们无法与主机 URL {enterpriseSearchUrl} 的 Enterprise Search 建立连接:", "xpack.enterpriseSearch.errorConnectingState.description2": "确保在 {configFile} 中已正确配置主机 URL。", @@ -11391,21 +11437,21 @@ "xpack.enterpriseSearch.content.newIndex.steps.configureIngestion.title": "配置采集设置", "xpack.enterpriseSearch.content.newIndex.steps.createIndex.crawler.title": "使用网络爬虫编制索引", "xpack.enterpriseSearch.content.newIndex.steps.createIndex.title": "创建 Elasticsearch 索引", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.chineseDropDownOptionLabel": "中文", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.danishDropDownOptionLabel": "丹麦语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.dutchDropDownOptionLabel": "荷兰语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.englishDropDownOptionLabel": "英语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.frenchDropDownOptionLabel": "法语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.germanDropDownOptionLabel": "德语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.italianDropDownOptionLabel": "意大利语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.japaneseDropDownOptionLabel": "日语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.koreanDropDownOptionLabel": "朝鲜语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseBrazilDropDownOptionLabel": "葡萄牙语(巴西)", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.portugueseDropDownOptionLabel": "葡萄牙语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.russianDropDownOptionLabel": "俄语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.spanishDropDownOptionLabel": "西班牙语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.thaiDropDownOptionLabel": "泰语", - "xpack.enterpriseSearch.content.newIndex.supportedLanguages.universalDropDownOptionLabel": "通用", + "xpack.enterpriseSearch.content.supportedLanguages.chineseLabel": "中文", + "xpack.enterpriseSearch.content.supportedLanguages.danishLabel": "丹麦语", + "xpack.enterpriseSearch.content.supportedLanguages.dutchLabel": "荷兰语", + "xpack.enterpriseSearch.content.supportedLanguages.englishLabel": "英语", + "xpack.enterpriseSearch.content.supportedLanguages.frenchLabel": "法语", + "xpack.enterpriseSearch.content.supportedLanguages.germanLabel": "德语", + "xpack.enterpriseSearch.content.supportedLanguages.italianLabel": "意大利语", + "xpack.enterpriseSearch.content.supportedLanguages.japaneseLabel": "日语", + "xpack.enterpriseSearch.content.supportedLanguages.koreanLabel": "朝鲜语", + "xpack.enterpriseSearch.content.supportedLanguages.portugueseBrazilLabel": "葡萄牙语(巴西)", + "xpack.enterpriseSearch.content.supportedLanguages.portugueseLabel": "葡萄牙语", + "xpack.enterpriseSearch.content.supportedLanguages.russianLabel": "俄语", + "xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "西班牙语", + "xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "泰语", + "xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "通用", "xpack.enterpriseSearch.content.newIndex.types.api": "API 终端", "xpack.enterpriseSearch.content.newIndex.types.connector": "连接器", "xpack.enterpriseSearch.content.newIndex.types.crawler": "网络爬虫", @@ -11495,13 +11541,10 @@ "xpack.enterpriseSearch.crawler.addDomainForm.urlLabel": "域 URL", "xpack.enterpriseSearch.crawler.addDomainForm.validateButtonLabel": "验证域", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlAutomaticallySwitchLabel": "自动爬网", - "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.crawlUnitsPrefix": "每", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.readMoreLink": "阅读更多内容。", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleDescription": "爬网计划将对此索引上的每个域执行全面爬网。", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleFrequencyLabel": "计划频率", "xpack.enterpriseSearch.crawler.automaticCrawlSchedule.scheduleUnitsLabel": "计划时间单位", - "xpack.enterpriseSearch.crawler.automaticCrawlScheduler.disableCrawlSchedule.successMessage": "自动爬网已禁用。", - "xpack.enterpriseSearch.crawler.automaticCrawlScheduler.submitCrawlSchedule.successMessage": "您的自动爬网计划已更新。", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlDepthLabel": "最大爬网深度", "xpack.enterpriseSearch.crawler.components.crawlDetailsSummary.crawlTypeLabel": "爬网类型", "xpack.enterpriseSearch.crawler.crawlCustomSettingsFlyout.customEntryPointUrlsTextboxLabel": "定制入口点 URL", diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index 0673e28888d10..00310d7a69938 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -51,6 +51,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.fleet.packages.0.version=latest`, // set the packagerTaskInterval to 5s in order to speed up test executions when checking fleet artifacts '--xpack.securitySolution.packagerTaskInterval=5s', + // this will be removed in 8.7 when the file upload feature is released + `--xpack.fleet.enableExperimental.0=diagnosticFileUploadEnabled`, ], }, layout: { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/file_upload_index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/file_upload_index.ts new file mode 100644 index 0000000000000..ce8179a050c47 --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/file_upload_index.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 expect from '@kbn/expect'; +import { + FILE_STORAGE_DATA_INDEX, + FILE_STORAGE_METADATA_INDEX, +} from '@kbn/security-solution-plugin/common/endpoint/constants'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esClient = getService('es'); + + describe('File upload indices', () => { + it('should have created the file data index on install', async () => { + const endpointFileUploadIndexExists = await esClient.indices.exists({ + index: FILE_STORAGE_METADATA_INDEX, + }); + + expect(endpointFileUploadIndexExists).equal(true); + }); + it('should have created the files index on install', async () => { + const endpointFileUploadIndexExists = await esClient.indices.exists({ + index: FILE_STORAGE_DATA_INDEX, + }); + + expect(endpointFileUploadIndexExists).equal(true); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 7be4ce2243303..22a7a5d7567ef 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -32,6 +32,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./package')); loadTestFile(require.resolve('./endpoint_authz')); + loadTestFile(require.resolve('./file_upload_index')); loadTestFile(require.resolve('./endpoint_artifacts/trusted_apps')); loadTestFile(require.resolve('./endpoint_artifacts/event_filters')); loadTestFile(require.resolve('./endpoint_artifacts/host_isolation_exceptions')); diff --git a/x-pack/test/security_solution_endpoint_api_int/config.ts b/x-pack/test/security_solution_endpoint_api_int/config.ts index 6e3ca0d718b6e..505d9734593b9 100644 --- a/x-pack/test/security_solution_endpoint_api_int/config.ts +++ b/x-pack/test/security_solution_endpoint_api_int/config.ts @@ -29,6 +29,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // always install Endpoint package by default when Fleet sets up `--xpack.fleet.packages.0.name=endpoint`, `--xpack.fleet.packages.0.version=latest`, + // this will be removed in 8.7 when the file upload feature is released + `--xpack.fleet.enableExperimental.0=diagnosticFileUploadEnabled`, ], }, };