diff --git a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml index e641ed08bf34f..77ae40971a282 100644 --- a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml +++ b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml @@ -56,8 +56,8 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_serverless.sh - label: 'Serverless Security Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_serverless_entity_analytics.sh + label: 'Serverless Entity Analytics - Security Solution Cypress Tests' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" agents: queue: n2-4-spot diff --git a/.buildkite/pipelines/flaky_tests/groups.json b/.buildkite/pipelines/flaky_tests/groups.json index 992efad798b8d..292c5fe33397c 100644 --- a/.buildkite/pipelines/flaky_tests/groups.json +++ b/.buildkite/pipelines/flaky_tests/groups.json @@ -1,12 +1,12 @@ { "groups": [ { - "key": "cypress/security_solution", - "name": "Security Solution - Cypress" + "key": "cypress/security_solution_entity_analytics", + "name": "Security Solution Entity Analytics - Cypress" }, { - "key": "cypress/security_serverless", - "name": "[Serverless] Security Solution - Cypress" + "key": "cypress/security_serverless_entity_analytics", + "name": "[Serverless] Security Solution Entity Analytics - Cypress" }, { "key": "cypress/security_solution_investigations", @@ -32,7 +32,6 @@ "key": "cypress/security_serverless_rule_management", "name": "[Serverless] Security Solution Rule Management - Cypress" }, - { "key": "cypress/security_solution_rule_management_prebuilt_rules", "name": "Security Solution Rule Management - Prebuilt Rules - Cypress" diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index ed464e2a184f4..aa7a96bc8b4a8 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -79,8 +79,8 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_serverless.sh - label: 'Serverless Security Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_serverless_entity_analytics.sh + label: 'Serverless Entity Analytics - Security Cypress Tests' agents: queue: n2-4-spot depends_on: build @@ -235,13 +235,13 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_solution.sh - label: 'Security Solution Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_solution_entity_analytics.sh + label: 'Entity Analytics - Security Solution Cypress Tests' agents: queue: n2-4-spot depends_on: build timeout_in_minutes: 60 - parallelism: 8 + parallelism: 2 retry: automatic: - exit_status: '*' diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 0040767d5b3d1..223acd9220046 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -57,8 +57,8 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_serverless.sh - label: 'Serverless Security Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_serverless_entity_analytics.sh + label: 'Serverless Entity Analytics - Security Cypress Tests' agents: queue: n2-4-spot depends_on: build @@ -153,13 +153,13 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_solution.sh - label: 'Security Solution Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_solution_entity_analytics.sh + label: 'Entity Analytics - Security Solution Cypress Tests' agents: queue: n2-4-spot depends_on: build timeout_in_minutes: 60 - parallelism: 8 + parallelism: 2 retry: automatic: - exit_status: '*' diff --git a/.buildkite/pipelines/security_solution/security_solution_cypress.yml b/.buildkite/pipelines/security_solution/security_solution_cypress.yml index 3fdd8c16a9b76..58e505927b95c 100644 --- a/.buildkite/pipelines/security_solution/security_solution_cypress.yml +++ b/.buildkite/pipelines/security_solution/security_solution_cypress.yml @@ -93,4 +93,16 @@ steps: retry: automatic: - exit_status: '*' - limit: 1 \ No newline at end of file + limit: 1 + + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:entity_analytics + label: 'Serverless MKI QA Entity Analytics - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 2 + retry: + automatic: + - exit_status: '*' + limit: 1 \ No newline at end of file diff --git a/.buildkite/scripts/steps/functional/security_serverless.sh b/.buildkite/scripts/steps/functional/security_serverless_entity_analytics.sh similarity index 67% rename from .buildkite/scripts/steps/functional/security_serverless.sh rename to .buildkite/scripts/steps/functional/security_serverless_entity_analytics.sh index 9903f44da0373..0c3822e149d53 100644 --- a/.buildkite/scripts/steps/functional/security_serverless.sh +++ b/.buildkite/scripts/steps/functional/security_serverless_entity_analytics.sh @@ -8,9 +8,9 @@ source .buildkite/scripts/steps/functional/common_cypress.sh export JOB=kibana-serverless-security-cypress export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} -echo "--- Security Serverless Cypress Tests" +echo "--- Entity Analytics Cypress Tests on Serverless" cd x-pack/test/security_solution_cypress set +e -yarn cypress:run:serverless; status=$?; yarn junit:merge || :; exit $status +yarn cypress:entity_analytics:run:serverless; status=$?; yarn junit:merge || :; exit $status diff --git a/.buildkite/scripts/steps/functional/security_solution.sh b/.buildkite/scripts/steps/functional/security_solution_entity_analytics.sh similarity index 66% rename from .buildkite/scripts/steps/functional/security_solution.sh rename to .buildkite/scripts/steps/functional/security_solution_entity_analytics.sh index fdba86f4dee4e..09f44d4c6b7ab 100755 --- a/.buildkite/scripts/steps/functional/security_solution.sh +++ b/.buildkite/scripts/steps/functional/security_solution_entity_analytics.sh @@ -8,9 +8,9 @@ source .buildkite/scripts/steps/functional/common_cypress.sh export JOB=kibana-security-solution-chrome export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} -echo "--- Security Solution Cypress tests (Chrome)" +echo "--- Entity Analytics - Security Solution Cypress Tests" cd x-pack/test/security_solution_cypress set +e -yarn cypress:run:ess; status=$?; yarn junit:merge || :; exit $status +yarn cypress:entity_analytics:run:ess; status=$?; yarn junit:merge || :; exit $status diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 366808adc6ac2..86d597b88c20c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1481,6 +1481,7 @@ x-pack/plugins/security_solution/public/overview/components/entity_analytics x-pack/plugins/security_solution/server/lib/entity_analytics @elastic/security-entity-analytics x-pack/plugins/security_solution/server/lib/risk_score @elastic/security-entity-analytics x-pack/test/security_solution_api_integration/test_suites/entity_analytics @elastic/security-entity-analytics +x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics @elastic/security-entity-analytics x-pack/plugins/security_solution/public/flyout/entity_details @elastic/security-entity-analytics x-pack/plugins/security_solution/common/api/entity_analytics @elastic/security-entity-analytics /x-pack/plugins/security_solution/public/entity_analytics @elastic/security-entity-analytics diff --git a/dev_docs/shared_ux/shared_ux_landing.mdx b/dev_docs/shared_ux/shared_ux_landing.mdx new file mode 100644 index 0000000000000..2b6bc7661dedd --- /dev/null +++ b/dev_docs/shared_ux/shared_ux_landing.mdx @@ -0,0 +1,70 @@ +--- +id: kibDevDocsSharedUxOverview +slug: /kibana-dev-docs/shared-ux +title: Shared UX Team +description: Links to all the documentation maintained by the Shared UX team. +tags: ['kibana', 'dev', 'shared-ux'] +layout: landing +--- + + + + + + diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index edff2531ab96f..6ff5cef8300e6 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -10,6 +10,7 @@ Review important information about the {kib} 8.x releases. +* <> * <> * <> * <> @@ -55,6 +56,25 @@ Review important information about the {kib} 8.x releases. * <> -- +[[release-notes-8.11.3]] +== {kib} 8.11.3 + +The 8.11.3 release includes the following bug fixes. + +[float] +[[fixes-v8.11.3]] +=== Bug Fixes +Elastic Security:: +For the Elastic Security 8.11.3 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Fleet:: +* Fixes a 500 error in the Fleet API when a request for the product versions endpoint throws `ECONNREFUSED` ({kibana-pull}172850[#172850]). +* Fixes agent policy timeout to accept only integers ({kibana-pull}172222[#172222]). +Machine Learning:: +* Fixes data drift numeric fields not displaying correctly ({kibana-pull}172504[#172504]). +* Fixes Data visualizer, ML field stats, and Data Frame Analytics so the `_tier` field can be excluded ({kibana-pull}172223[#172223]). +Operations:: +* Fixes an issue where running `kibana-keystore` commands required `kibana.yml` to exist ({kibana-pull}172943[#172943]). + [[release-notes-8.11.2]] == {kib} 8.11.2 @@ -105,7 +125,7 @@ SharedUX:: * Fixes the custom threshold document link ({kibana-pull}171125[#171125]). Uptime:: * Fixes advanced fields broken for ICMP monitors ({kibana-pull}171161[#171161]). - +Ê [[release-notes-8.11.1]] == {kib} 8.11.1 diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index b724286d5bac3..d4eb3aaba6dd9 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -25,15 +25,6 @@ } ] }, - { - "label": "Operations", - "items": [ - { - "id": "kibDevDocsOpsOverview", - "label": "Overview" - } - ] - }, { "label": "Contributing", "items": [ @@ -149,9 +140,6 @@ { "id": "kibDevTutorialExpressions" }, - { - "id": "kibDevDocsKPTTutorial" - }, { "id": "kibDevTutorialDataSearchAndSessions", "label": "data.search" @@ -178,15 +166,21 @@ }, { "id": "kibDevTutorialAdvancedSettings" - }, + } + ] + }, + { + "label": "Serverless", + "pageId": "ktServerlessReleaseOverview", + "items": [ { - "id": "kibDevSharePluginReadme" + "id": "ktServerlessReleaseOverview" }, { - "id": "kibDevTutorialScreenshotting" + "id": "ktServerlessEmergencyReleases" }, { - "id": "kibDevTutorialsContentManagementOnboarding" + "id": "ktCustomServerlessImage" }, { "id": "kibDevTutorialsServerlessProjectNavigation" @@ -194,16 +188,54 @@ ] }, { - "label": "Serverless", + "label": "Operations", "items": [ { - "id": "ktServerlessReleaseOverview" + "id": "kibDevDocsOpsOverview", + "label": "Overview" + } + ] + }, + { + "label": "Shared UX", + "pageId": "kibDevDocsSharedUxOverview", + "items": [ + { + "label": "Serverless Project Navigation", + "id": "kibDevTutorialsServerlessProjectNavigation" }, { - "id": "ktServerlessEmergencyReleases" + "id": "kibDevTutorialFileService", + "label": "File service" }, { - "id": "ktCustomServerlessImage" + "id": "kibDevSharePluginReadme", + "label": "Sharing" + }, + { + "label": "Content Management", + "id": "kibContentManagement", + "items": [ + { + "id": "kibDevTutorialsContentManagementOnboarding" + } + ] + }, + { + "id": "kibDevTutorialScreenshotting", + "label": "Screenshotting" + }, + { + "id": "kibDevTutorialAdvancedSettings", + "label": "Advanced Settings" + }, + { + "id": "kibDevDocsKPTTutorial", + "label": "Kibana Page Template" + }, + { + "id": "kibDevReactKibanaContext", + "label": "Kibana React Contexts" } ] }, diff --git a/packages/deeplinks/observability/constants.ts b/packages/deeplinks/observability/constants.ts index a28c9e4aaff21..9f2a28fcef971 100644 --- a/packages/deeplinks/observability/constants.ts +++ b/packages/deeplinks/observability/constants.ts @@ -8,7 +8,7 @@ export const LOGS_APP_ID = 'logs'; -export const OBSERVABILITY_LOG_EXPLORER = 'observability-log-explorer'; +export const OBSERVABILITY_LOG_EXPLORER_APP_ID = 'observability-log-explorer'; export const OBSERVABILITY_OVERVIEW_APP_ID = 'observability-overview'; diff --git a/packages/deeplinks/observability/deep_links.ts b/packages/deeplinks/observability/deep_links.ts index 844d68fdb27cc..831976bcc37ea 100644 --- a/packages/deeplinks/observability/deep_links.ts +++ b/packages/deeplinks/observability/deep_links.ts @@ -7,16 +7,16 @@ */ import { + APM_APP_ID, LOGS_APP_ID, - OBSERVABILITY_LOG_EXPLORER, - OBSERVABILITY_OVERVIEW_APP_ID, METRICS_APP_ID, - APM_APP_ID, + OBSERVABILITY_LOG_EXPLORER_APP_ID, OBSERVABILITY_ONBOARDING_APP_ID, + OBSERVABILITY_OVERVIEW_APP_ID, } from './constants'; type LogsApp = typeof LOGS_APP_ID; -type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER; +type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER_APP_ID; type ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID; type MetricsApp = typeof METRICS_APP_ID; type ApmApp = typeof APM_APP_ID; diff --git a/packages/deeplinks/observability/index.ts b/packages/deeplinks/observability/index.ts index 81dac13f6b6a0..3d816db884765 100644 --- a/packages/deeplinks/observability/index.ts +++ b/packages/deeplinks/observability/index.ts @@ -7,12 +7,10 @@ */ export { - OBSERVABILITY_ONBOARDING_APP_ID, LOGS_APP_ID, - OBSERVABILITY_LOG_EXPLORER, + OBSERVABILITY_LOG_EXPLORER_APP_ID, + OBSERVABILITY_ONBOARDING_APP_ID, OBSERVABILITY_OVERVIEW_APP_ID, } from './constants'; - export type { AppId, DeepLinkId } from './deep_links'; - export * from './locators'; diff --git a/packages/deeplinks/observability/locators/log_explorer.ts b/packages/deeplinks/observability/locators/log_explorer.ts index 752ae3d79bee8..d20c36e73917d 100644 --- a/packages/deeplinks/observability/locators/log_explorer.ts +++ b/packages/deeplinks/observability/locators/log_explorer.ts @@ -14,6 +14,17 @@ export type RefreshInterval = { value: number; }; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type FilterControls = { + namespace?: ListFilterControl; +}; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type ListFilterControl = { + mode: 'include'; + values: string[]; +}; + export const LOG_EXPLORER_LOCATOR_ID = 'LOG_EXPLORER_LOCATOR'; export interface LogExplorerNavigationParams extends SerializableRecord { @@ -34,13 +45,13 @@ export interface LogExplorerNavigationParams extends SerializableRecord { */ columns?: string[]; /** - * Array of the used sorting [[field,direction],...] + * Optionally apply free-form filters. */ - sort?: string[][]; + filters?: Filter[]; /** - * Optionally apply filters. + * Optionally apply curated filter controls */ - filters?: Filter[]; + filterControls?: FilterControls; } export interface LogExplorerLocatorParams extends LogExplorerNavigationParams { diff --git a/packages/kbn-apm-synthtrace-client/src/types/agent_names.ts b/packages/kbn-apm-synthtrace-client/src/types/agent_names.ts index d9e3a371e0e87..c57b15e3dace0 100644 --- a/packages/kbn-apm-synthtrace-client/src/types/agent_names.ts +++ b/packages/kbn-apm-synthtrace-client/src/types/agent_names.ts @@ -31,6 +31,7 @@ type OpenTelemetryAgentName = | 'opentelemetry/python' | 'opentelemetry/ruby' | 'opentelemetry/swift' + | 'opentelemetry/android' | 'opentelemetry/webjs'; // Unable to reference AgentName from '@kbn/apm-plugin/typings/es_schemas/ui/fields/agent' due to circular reference diff --git a/packages/kbn-apm-synthtrace/src/scenarios/malformed_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts similarity index 94% rename from packages/kbn-apm-synthtrace/src/scenarios/malformed_logs.ts rename to packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts index 55b32e592d62c..cb5313ac70795 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/malformed_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts @@ -29,12 +29,6 @@ const scenario: Scenario = async (runOptions) => { const CLOUD_PROVIDERS = ['gcp', 'aws', 'azure']; const CLOUD_REGION = ['eu-central-1', 'us-east-1', 'area-51']; - // "ignore_above": 1024 in mapping - const MALFORMED_LOG_LEVEL = MORE_THAN_1024_CHARS; - - // "ignore_above": 1024 in mapping - const MALFORMED_CLOUD_REGION = MORE_THAN_1024_CHARS; - const CLUSTER = [ { clusterId: generateShortId(), clusterName: 'synth-cluster-1' }, { clusterId: generateShortId(), clusterName: 'synth-cluster-2' }, @@ -76,7 +70,7 @@ const scenario: Scenario = async (runOptions) => { .create() .dataset('synth.2') .message(MESSAGE_LOG_LEVELS[index].message as string) - .logLevel(isMalformed ? MALFORMED_LOG_LEVEL : MESSAGE_LOG_LEVELS[index].level) + .logLevel(isMalformed ? MORE_THAN_1024_CHARS : MESSAGE_LOG_LEVELS[index].level) // "ignore_above": 1024 in mapping .service(SERVICE_NAMES[index]) .defaults({ 'trace.id': generateShortId(), @@ -101,7 +95,7 @@ const scenario: Scenario = async (runOptions) => { .create() .dataset('synth.3') .message(MESSAGE_LOG_LEVELS[index].message as string) - .logLevel(isMalformed ? MALFORMED_LOG_LEVEL : MESSAGE_LOG_LEVELS[index].level) + .logLevel(isMalformed ? MORE_THAN_1024_CHARS : MESSAGE_LOG_LEVELS[index].level) // "ignore_above": 1024 in mapping .service(SERVICE_NAMES[index]) .defaults({ 'trace.id': generateShortId(), @@ -112,7 +106,7 @@ const scenario: Scenario = async (runOptions) => { 'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)], 'cloud.region': CLOUD_REGION[index], 'cloud.availability_zone': isMalformed - ? MALFORMED_CLOUD_REGION + ? MORE_THAN_1024_CHARS // "ignore_above": 1024 in mapping : `${CLOUD_REGION[index]}a`, 'cloud.project.id': generateShortId(), 'cloud.instance.id': generateShortId(), diff --git a/packages/kbn-elastic-agent-utils/src/agent_names.ts b/packages/kbn-elastic-agent-utils/src/agent_names.ts index f29160699a241..ca98b64024f8a 100644 --- a/packages/kbn-elastic-agent-utils/src/agent_names.ts +++ b/packages/kbn-elastic-agent-utils/src/agent_names.ts @@ -49,6 +49,7 @@ export type OpenTelemetryAgentName = | 'opentelemetry/ruby' | 'opentelemetry/rust' | 'opentelemetry/swift' + | 'opentelemetry/android' | 'opentelemetry/webjs'; export const OPEN_TELEMETRY_AGENT_NAMES: OpenTelemetryAgentName[] = [ 'otlp', @@ -63,6 +64,7 @@ export const OPEN_TELEMETRY_AGENT_NAMES: OpenTelemetryAgentName[] = [ 'opentelemetry/ruby', 'opentelemetry/rust', 'opentelemetry/swift', + 'opentelemetry/android', 'opentelemetry/webjs', ]; diff --git a/packages/kbn-es-query/index.ts b/packages/kbn-es-query/index.ts index ac0a078e34d94..cfcf8239196df 100644 --- a/packages/kbn-es-query/index.ts +++ b/packages/kbn-es-query/index.ts @@ -128,3 +128,5 @@ export { isDataViewFieldSubtypeMulti, isDataViewFieldSubtypeNested, } from './src/utils'; + +export type { ExecutionContextSearch } from './src/expressions/types'; diff --git a/packages/kbn-es-query/src/expressions/types.ts b/packages/kbn-es-query/src/expressions/types.ts new file mode 100644 index 0000000000000..42dc021c9b752 --- /dev/null +++ b/packages/kbn-es-query/src/expressions/types.ts @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Filter, Query, TimeRange } from '../filters'; + +export interface ExecutionContextSearch { + filters?: Filter[]; + query?: Query | Query[]; + timeRange?: TimeRange; + disableWarningToasts?: boolean; +} diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index ef99652ee767f..764fe35dd759d 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -131,7 +131,7 @@ pageLoadAssetSize: securitySolutionServerless: 62488 serverless: 16573 serverlessObservability: 68747 - serverlessSearch: 71995 + serverlessSearch: 72995 sessionView: 77750 share: 71239 snapshotRestore: 79032 diff --git a/packages/kbn-search-index-documents/lib/fetch_search_results.test.ts b/packages/kbn-search-index-documents/lib/fetch_search_results.test.ts index dc60dcba437bd..dec058aae0bf2 100644 --- a/packages/kbn-search-index-documents/lib/fetch_search_results.test.ts +++ b/packages/kbn-search-index-documents/lib/fetch_search_results.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IScopedClusterClient } from '@kbn/core/server'; +import { ElasticsearchClient } from '@kbn/core/server'; import { DEFAULT_DOCS_PER_PAGE } from '../types'; import { fetchSearchResults } from './fetch_search_results'; @@ -14,9 +14,7 @@ import { fetchSearchResults } from './fetch_search_results'; const DEFAULT_FROM_VALUE = 0; describe('fetchSearchResults lib function', () => { const mockClient = { - asCurrentUser: { - search: jest.fn(), - }, + search: jest.fn(), }; const indexName = 'search-regular-index'; @@ -78,15 +76,13 @@ describe('fetchSearchResults lib function', () => { }); it('should return search results with hits', async () => { - mockClient.asCurrentUser.search.mockImplementation(() => - Promise.resolve(mockSearchResponseWithHits) - ); + mockClient.search.mockImplementation(() => Promise.resolve(mockSearchResponseWithHits)); await expect( - fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName, query) + fetchSearchResults(mockClient as unknown as ElasticsearchClient, indexName, query) ).resolves.toEqual(regularSearchResultsResponse); - expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({ + expect(mockClient.search).toHaveBeenCalledWith({ from: DEFAULT_FROM_VALUE, index: indexName, q: query, @@ -95,13 +91,11 @@ describe('fetchSearchResults lib function', () => { }); it('should escape quotes in queries and return results with hits', async () => { - mockClient.asCurrentUser.search.mockImplementation(() => - Promise.resolve(mockSearchResponseWithHits) - ); + mockClient.search.mockImplementation(() => Promise.resolve(mockSearchResponseWithHits)); await expect( fetchSearchResults( - mockClient as unknown as IScopedClusterClient, + mockClient as unknown as ElasticsearchClient, indexName, '"yellow banana"', 0, @@ -109,7 +103,7 @@ describe('fetchSearchResults lib function', () => { ) ).resolves.toEqual(regularSearchResultsResponse); - expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({ + expect(mockClient.search).toHaveBeenCalledWith({ from: DEFAULT_FROM_VALUE, index: indexName, q: '\\"yellow banana\\"', @@ -118,15 +112,13 @@ describe('fetchSearchResults lib function', () => { }); it('should return search results with hits when no query is passed', async () => { - mockClient.asCurrentUser.search.mockImplementation(() => - Promise.resolve(mockSearchResponseWithHits) - ); + mockClient.search.mockImplementation(() => Promise.resolve(mockSearchResponseWithHits)); await expect( - fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName) + fetchSearchResults(mockClient as unknown as ElasticsearchClient, indexName) ).resolves.toEqual(regularSearchResultsResponse); - expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({ + expect(mockClient.search).toHaveBeenCalledWith({ from: DEFAULT_FROM_VALUE, index: indexName, size: DEFAULT_DOCS_PER_PAGE, @@ -134,7 +126,7 @@ describe('fetchSearchResults lib function', () => { }); it('should return empty search results', async () => { - mockClient.asCurrentUser.search.mockImplementationOnce(() => + mockClient.search.mockImplementationOnce(() => Promise.resolve({ ...mockSearchResponseWithHits, hits: { @@ -149,10 +141,10 @@ describe('fetchSearchResults lib function', () => { ); await expect( - fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName, query) + fetchSearchResults(mockClient as unknown as ElasticsearchClient, indexName, query) ).resolves.toEqual(emptySearchResultsResponse); - expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({ + expect(mockClient.search).toHaveBeenCalledWith({ from: DEFAULT_FROM_VALUE, index: indexName, q: query, diff --git a/packages/kbn-search-index-documents/lib/fetch_search_results.ts b/packages/kbn-search-index-documents/lib/fetch_search_results.ts index 4427fded71c2c..3abca93516c5a 100644 --- a/packages/kbn-search-index-documents/lib/fetch_search_results.ts +++ b/packages/kbn-search-index-documents/lib/fetch_search_results.ts @@ -7,13 +7,13 @@ */ import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; -import { IScopedClusterClient } from '@kbn/core/server'; +import { ElasticsearchClient } from '@kbn/core/server'; import { DEFAULT_DOCS_PER_PAGE, Paginate } from '../types'; import { escapeLuceneChars } from '../utils/escape_lucene_charts'; import { fetchWithPagination } from '../utils/fetch_with_pagination'; export const fetchSearchResults = async ( - client: IScopedClusterClient, + client: ElasticsearchClient, indexName: string, query?: string, from: number = 0, @@ -21,7 +21,7 @@ export const fetchSearchResults = async ( ): Promise> => { const result = await fetchWithPagination( async () => - await client.asCurrentUser.search({ + await client.search({ from, index: indexName, size, diff --git a/packages/kbn-xstate-utils/src/dev_tools.ts b/packages/kbn-xstate-utils/src/dev_tools.ts index fa16b808b3aec..15b5779677a5b 100644 --- a/packages/kbn-xstate-utils/src/dev_tools.ts +++ b/packages/kbn-xstate-utils/src/dev_tools.ts @@ -6,4 +6,52 @@ * Side Public License, v 1. */ +import { + isArray, + isBoolean, + isDate, + isNil, + isNumber, + isPlainObject, + isString, + mapValues, +} from 'lodash'; + export const isDevMode = () => process.env.NODE_ENV !== 'production'; + +export const getDevToolsOptions = (): boolean | object => + isDevMode() + ? { + actionSanitizer: sanitizeAction, + stateSanitizer: sanitizeState, + } + : false; + +const redactComplexValues = (value: unknown): unknown => { + if (isString(value) || isNumber(value) || isBoolean(value) || isDate(value) || isNil(value)) { + return value; + } + + if (isArray(value)) { + if (value.length > 100) { + return '[redacted large array]'; + } + return value.map(redactComplexValues); + } + + if ((isPlainObject as (v: unknown) => v is object)(value)) { + if (Object.keys(value).length > 100) { + return '[redacted large object]'; + } + return mapValues(value, (innerValue: unknown) => redactComplexValues(innerValue)); + } + + return `[redacted complex value of type ${typeof value}]`; +}; + +const sanitizeAction = redactComplexValues; + +const sanitizeState = (state: Record) => ({ + value: state.value, + context: redactComplexValues(state.context), +}); diff --git a/packages/kbn-xstate-utils/src/index.ts b/packages/kbn-xstate-utils/src/index.ts index 2cf5853db6e08..02cd8a2b176b1 100644 --- a/packages/kbn-xstate-utils/src/index.ts +++ b/packages/kbn-xstate-utils/src/index.ts @@ -7,6 +7,6 @@ */ export * from './actions'; +export * from './dev_tools'; export * from './notification_channel'; export * from './types'; -export * from './dev_tools'; diff --git a/packages/react/kibana_context/README.mdx b/packages/react/kibana_context/README.mdx index 28fd9ecb1600f..8738461245e65 100644 --- a/packages/react/kibana_context/README.mdx +++ b/packages/react/kibana_context/README.mdx @@ -1,9 +1,9 @@ --- -id: react/context -slug: /react/context +id: kibDevReactKibanaContext +slug: /kibana-dev-docs/react-context title: React Contexts in Kibana description: Kibana uses React Context to manage several global states. This is a collection of packages supporting those states. -tags: ['shared-ux', 'react', 'context'] +tags: ['kibana', 'dev','shared-ux', 'react', 'context'] date: 2023-07-25 --- diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts index 6dcfe4c79147a..30f55a9679269 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_trendline_function.test.ts @@ -8,11 +8,10 @@ import { Datatable, ExecutionContext } from '@kbn/expressions-plugin/common'; import { Adapters } from '@kbn/inspector-plugin/common'; -import { SerializableRecord } from '@kbn/utility-types'; import { TrendlineArguments } from '../types'; import { metricTrendlineFunction } from './metric_trendline_function'; -const fakeContext = {} as ExecutionContext; +const fakeContext = {} as ExecutionContext; const fakeInput = {} as Datatable; const metricTrendline = (args: TrendlineArguments) => metricTrendlineFunction().fn(fakeInput, args, fakeContext); diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/inference.delete_model.json b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.delete_model.json new file mode 100644 index 0000000000000..c7066c0a48b19 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.delete_model.json @@ -0,0 +1,27 @@ +{ + "inference.delete_model": { + "url_params": { + "error_trace": "__flag__", + "filter_path": [], + "human": "__flag__", + "pretty": "__flag__" + }, + "url_components": { + "task_type": [ + "sparse_embedding", + "text_embedding" + ] + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_inference/{task_type}/{model_id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/delete-inference-api.html", + "availability": { + "stack": true, + "serverless": false + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/inference.get_model.json b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.get_model.json new file mode 100644 index 0000000000000..40745a385fa75 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.get_model.json @@ -0,0 +1,27 @@ +{ + "inference.get_model": { + "url_params": { + "error_trace": "__flag__", + "filter_path": [], + "human": "__flag__", + "pretty": "__flag__" + }, + "url_components": { + "task_type": [ + "sparse_embedding", + "text_embedding" + ] + }, + "methods": [ + "GET" + ], + "patterns": [ + "_inference/{task_type}/{model_id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/get-inference-api.html", + "availability": { + "stack": true, + "serverless": false + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/inference.inference.json b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.inference.json new file mode 100644 index 0000000000000..e8f959aa3330a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.inference.json @@ -0,0 +1,27 @@ +{ + "inference.inference": { + "url_params": { + "error_trace": "__flag__", + "filter_path": [], + "human": "__flag__", + "pretty": "__flag__" + }, + "url_components": { + "task_type": [ + "sparse_embedding", + "text_embedding" + ] + }, + "methods": [ + "POST" + ], + "patterns": [ + "_inference/{task_type}/{model_id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/post-inference-api.html", + "availability": { + "stack": true, + "serverless": false + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/inference.put_model.json b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.put_model.json new file mode 100644 index 0000000000000..69e26a1103c02 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/inference.put_model.json @@ -0,0 +1,27 @@ +{ + "inference.put_model": { + "url_params": { + "error_trace": "__flag__", + "filter_path": [], + "human": "__flag__", + "pretty": "__flag__" + }, + "url_components": { + "task_type": [ + "sparse_embedding", + "text_embedding" + ] + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_inference/{task_type}/{model_id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/put-inference-api.html", + "availability": { + "stack": true, + "serverless": false + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_trained_model_deployment.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_trained_model_deployment.json index 972ae313c20fb..f09c45526e4f2 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_trained_model_deployment.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_trained_model_deployment.json @@ -6,6 +6,7 @@ "human": "__flag__", "pretty": "__flag__", "cache_size": [], + "deployment_id": "", "number_of_allocations": [ "1" ], diff --git a/src/plugins/content_management/docs/conent_management_landing.mdx b/src/plugins/content_management/docs/conent_management_landing.mdx new file mode 100644 index 0000000000000..b2855d105c7db --- /dev/null +++ b/src/plugins/content_management/docs/conent_management_landing.mdx @@ -0,0 +1,22 @@ +--- +id: kibContentManagement +slug: /kibana-dev-docs/content-management +title: Content Management +date: 2023-04-13 +tags: ['kibana', 'dev', 'content management', 'saved objects'] +--- + + + +Kibana Saved Objects have traditionally been considered content, +but the content management project aims to provide a single abstraction for Kibana and solution content. +This includes the registration and collection of content metadata as well as the rendering capabilities, caching layer, backward compatible CRUD & Search APIs, +and eventually a collection of new features that enhance content items by adding additional capabilities for personalization, private objects and content folders. + +## Links + +- +- diff --git a/src/plugins/data/common/search/expressions/kibana.ts b/src/plugins/data/common/search/expressions/kibana.ts index aa83662e0b8d4..83d2cdc1c64b9 100644 --- a/src/plugins/data/common/search/expressions/kibana.ts +++ b/src/plugins/data/common/search/expressions/kibana.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { ExecutionContext, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { Adapters } from '@kbn/inspector-plugin/common'; -import { ExpressionValueSearchContext, ExecutionContextSearch } from './kibana_context_type'; +import { ExpressionValueSearchContext } from './kibana_context_type'; const toArray = (query: undefined | T | T[]): T[] => !query ? [] : Array.isArray(query) ? query : [query]; @@ -20,7 +20,7 @@ export type ExpressionFunctionKibana = ExpressionFunctionDefinition< ExpressionValueSearchContext | null, object, ExpressionValueSearchContext, - ExecutionContext + ExecutionContext >; export const kibana: ExpressionFunctionKibana = { diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index 9f9a7f274ab5b..613a8ff2fede5 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -8,13 +8,7 @@ import { ExpressionFunctionDefinition, ExecutionContext } from '@kbn/expressions-plugin/common'; import { Adapters } from '@kbn/inspector-plugin/common'; -import { - KibanaTimerangeOutput, - ExecutionContextSearch, - KibanaContext, - KibanaFilter, - KibanaQueryOutput, -} from '../..'; +import { KibanaTimerangeOutput, KibanaContext, KibanaFilter, KibanaQueryOutput } from '../..'; interface Arguments { q?: KibanaQueryOutput[] | null; @@ -28,5 +22,5 @@ export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition< KibanaContext | null, Arguments, Promise, - ExecutionContext + ExecutionContext >; diff --git a/src/plugins/data/common/search/expressions/kibana_context_type.ts b/src/plugins/data/common/search/expressions/kibana_context_type.ts index d6947d2d46ce3..979fc65e4ac08 100644 --- a/src/plugins/data/common/search/expressions/kibana_context_type.ts +++ b/src/plugins/data/common/search/expressions/kibana_context_type.ts @@ -5,19 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { Filter } from '@kbn/es-query'; +import { Filter, ExecutionContextSearch } from '@kbn/es-query'; import { ExpressionValueBoxed } from '@kbn/expressions-plugin/common'; -import { Query, TimeRange } from '../../query'; +import { Query } from '../../query'; import { DataViewField } from '../..'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type ExecutionContextSearch = { - filters?: Filter[]; - query?: Query | Query[]; - timeRange?: TimeRange; - disableWarningToasts?: boolean; -}; - export type ExpressionValueSearchContext = ExpressionValueBoxed< 'kibana_context', ExecutionContextSearch diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 221a4f7ce9517..de2504dec2d99 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -138,7 +138,6 @@ export type { OptionedValueProp, ParsedInterval, // expressions - ExecutionContextSearch, ExpressionFunctionKql, ExpressionFunctionLucene, ExpressionFunctionKibana, diff --git a/src/plugins/data/server/query/routes.ts b/src/plugins/data/server/query/routes.ts index 7446e3ad83a12..5ac18df37d544 100644 --- a/src/plugins/data/server/query/routes.ts +++ b/src/plugins/data/server/query/routes.ts @@ -30,7 +30,7 @@ const SAVED_QUERY_ATTRS_CONFIG = schema.object({ const savedQueryResponseSchema = schema.object({ id: schema.string(), attributes: SAVED_QUERY_ATTRS_CONFIG, - namespaces: schema.arrayOf(schema.string()), + namespaces: schema.maybe(schema.arrayOf(schema.string())), }); const access = 'internal'; diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 5ab638a087783..46794e0a3c9c6 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -385,6 +385,13 @@ export function getUiSettings( defaultMessage: 'This week', }), }, + { + from: 'now-1m', + to: 'now', + display: i18n.translate('data.advancedSettings.timepicker.last1Minute', { + defaultMessage: 'Last 1 minute', + }), + }, { from: 'now-15m', to: 'now', @@ -471,7 +478,7 @@ export function getUiSettings( to: schema.string(), display: schema.string(), }), - { maxSize: 10 } + { maxSize: 12 } ) : schema.arrayOf( schema.object({ diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 1bb24661da0cb..2a2575a773524 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -9,7 +9,11 @@ import React, { useEffect, useState, memo, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { redirectWhenMissing, SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; +import { + type IKbnUrlStateStorage, + redirectWhenMissing, + SavedObjectNotFound, +} from '@kbn/kibana-utils-plugin/public'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import { AnalyticsNoDataPageKibanaProvider, @@ -46,6 +50,7 @@ interface DiscoverLandingParams { export interface MainRouteProps { customizationCallbacks: CustomizationCallback[]; + stateStorageContainer?: IKbnUrlStateStorage; isDev: boolean; customizationContext: DiscoverCustomizationContext; } @@ -53,6 +58,7 @@ export interface MainRouteProps { export function DiscoverMainRoute({ customizationCallbacks, customizationContext, + stateStorageContainer, }: MainRouteProps) { const history = useHistory(); const services = useDiscoverServices(); @@ -70,6 +76,7 @@ export function DiscoverMainRoute({ history, services, customizationContext, + stateStorageContainer, }) ); const { customizationService, isInitialized: isCustomizationServiceInitialized } = diff --git a/src/plugins/discover/public/application/main/services/discover_state.test.ts b/src/plugins/discover/public/application/main/services/discover_state.test.ts index a14a7d3b797c6..2cf1e2c71880d 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.test.ts @@ -11,7 +11,7 @@ import { DiscoverStateContainer, createSearchSessionRestorationDataProvider, } from './discover_state'; -import { createBrowserHistory, History } from 'history'; +import { createBrowserHistory, createMemoryHistory, History } from 'history'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; import { @@ -27,6 +27,7 @@ import { waitFor } from '@testing-library/react'; import { DiscoverCustomizationContext, FetchStatus } from '../../types'; import { dataViewAdHoc, dataViewComplexMock } from '../../../__mocks__/data_view_complex'; import { copySavedSearch } from './discover_saved_search_container'; +import { createKbnUrlStateStorage, IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; const startSync = (appState: DiscoverAppStateContainer) => { const { start, stop } = appState.syncState(); @@ -151,6 +152,68 @@ describe('Test discover state', () => { expect(getCurrentUrl()).toBe('/#?_g=(refreshInterval:(pause:!t,value:5000))'); }); }); + +describe('Test discover state with overridden state storage', () => { + let stopSync = () => {}; + let history: History; + let stateStorage: IKbnUrlStateStorage; + let state: DiscoverStateContainer; + + beforeEach(async () => { + jest.useFakeTimers(); + history = createMemoryHistory({ + initialEntries: [ + { + pathname: '/', + hash: `?_a=()`, + }, + ], + }); + stateStorage = createKbnUrlStateStorage({ + history, + useHash: false, + useHashQuery: true, + }); + state = getDiscoverStateContainer({ + services: discoverServiceMock, + history, + customizationContext, + stateStorageContainer: stateStorage, + }); + state.savedSearchState.set(savedSearchMock); + state.appState.update({}, true); + stopSync = startSync(state.appState); + }); + + afterEach(() => { + stopSync(); + stopSync = () => {}; + jest.useRealTimers(); + }); + + test('setting app state and syncing to URL', async () => { + state.appState.update({ index: 'modified' }); + + await jest.runAllTimersAsync(); + + expect(history.createHref(history.location)).toMatchInlineSnapshot( + `"/#?_a=(columns:!(default_column),index:modified,interval:auto,sort:!())"` + ); + }); + + test('changing URL to be propagated to appState', async () => { + history.push('/#?_a=(index:modified)'); + + await jest.runAllTimersAsync(); + + expect(state.appState.getState()).toMatchInlineSnapshot(` + Object { + "index": "modified", + } + `); + }); +}); + describe('Test discover initial state sort handling', () => { test('Non-empty sort in URL should not be overwritten by saved search sort', async () => { const savedSearch = { diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 5e4f8a30199ed..1dc58643ebdc0 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -71,6 +71,10 @@ interface DiscoverStateContainerParams { * Context object for customization related properties */ customizationContext: DiscoverCustomizationContext; + /** + * a custom url state storage + */ + stateStorageContainer?: IKbnUrlStateStorage; } export interface LoadParams { @@ -204,6 +208,7 @@ export function getDiscoverStateContainer({ history, services, customizationContext, + stateStorageContainer, }: DiscoverStateContainerParams): DiscoverStateContainer { const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage'); const toasts = services.core.notifications.toasts; @@ -211,12 +216,14 @@ export function getDiscoverStateContainer({ /** * state storage for state in the URL */ - const stateStorage = createKbnUrlStateStorage({ - useHash: storeInSessionStorage, - history, - useHashQuery: customizationContext.displayMode !== 'embedded', - ...(toasts && withNotifyOnErrors(toasts)), - }); + const stateStorage = + stateStorageContainer ?? + createKbnUrlStateStorage({ + useHash: storeInSessionStorage, + history, + useHashQuery: customizationContext.displayMode !== 'embedded', + ...(toasts && withNotifyOnErrors(toasts)), + }); /** * Search session logic diff --git a/src/plugins/discover/public/components/discover_container/discover_container.tsx b/src/plugins/discover/public/components/discover_container/discover_container.tsx index 43426c83c7730..51fdbd0d3432d 100644 --- a/src/plugins/discover/public/components/discover_container/discover_container.tsx +++ b/src/plugins/discover/public/components/discover_container/discover_container.tsx @@ -11,6 +11,7 @@ import type { ScopedHistory } from '@kbn/core/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import React, { useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; +import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { DiscoverMainRoute } from '../../application/main'; import type { DiscoverServices } from '../../build_services'; import type { CustomizationCallback } from '../../customizations'; @@ -29,6 +30,7 @@ export interface DiscoverContainerInternalProps { getDiscoverServices: () => Promise; scopedHistory: ScopedHistory; customizationCallbacks: CustomizationCallback[]; + stateStorageContainer?: IKbnUrlStateStorage; isDev: boolean; isLoading?: boolean; } @@ -55,6 +57,7 @@ export const DiscoverContainerInternal = ({ customizationCallbacks, isDev, getDiscoverServices, + stateStorageContainer, isLoading = false, }: DiscoverContainerInternalProps) => { const [discoverServices, setDiscoverServices] = useState(); @@ -97,6 +100,7 @@ export const DiscoverContainerInternal = ({ diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx index 2c9a30431218d..c24383ff9fb18 100644 --- a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx @@ -129,8 +129,9 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => { * Select state from the embeddable */ const loading = useSelectFromEmbeddableOutput('loading', embeddable); + const rendered = useSelectFromEmbeddableOutput('rendered', embeddable); - if (loading === false && !initialLoadComplete) { + if ((loading === false || rendered === true || outputError) && !initialLoadComplete) { setInitialLoadComplete(true); } diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts index 56748495a3456..36f5b0a5595a8 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts @@ -21,7 +21,6 @@ import { ExecutionContext } from '@kbn/expressions-plugin/common'; import moment from 'moment'; import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@elastic/charts'; import { Adapters } from '@kbn/inspector-plugin/common'; -import { SerializableRecord } from '@kbn/utility-types'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import { i18n } from '@kbn/i18n'; import { handleRequest } from './handle_request'; @@ -71,7 +70,7 @@ export const requestEventAnnotations = ( abortSignal, getSearchSessionId, getExecutionContext, - }: ExecutionContext, + }: ExecutionContext, getStartDependencies: () => Promise ) => { return defer(async () => { diff --git a/src/plugins/event_annotation/tsconfig.json b/src/plugins/event_annotation/tsconfig.json index d115df48e8967..f6117e5f92aed 100644 --- a/src/plugins/event_annotation/tsconfig.json +++ b/src/plugins/event_annotation/tsconfig.json @@ -12,7 +12,6 @@ "@kbn/core", "@kbn/expressions-plugin", "@kbn/data-plugin", - "@kbn/utility-types", "@kbn/i18n", "@kbn/data-views-plugin", "@kbn/inspector-plugin", diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index dddc503285942..03dbcc8a6ff13 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import type { SerializableRecord } from '@kbn/utility-types'; import type { KibanaRequest } from '@kbn/core/server'; import type { KibanaExecutionContext } from '@kbn/core/public'; import { Adapters, RequestAdapter } from '@kbn/inspector-plugin/common'; +import { ExecutionContextSearch } from '@kbn/es-query'; import { Datatable, ExpressionType } from '../expression_types'; import { TablesAdapter } from '../util/tables_adapter'; import { ExpressionsInspectorAdapter } from '../util'; @@ -19,10 +19,7 @@ import { ExpressionsInspectorAdapter } from '../util'; * `ExecutionContext` is an object available to all functions during a single execution; * it provides various methods to perform side-effects. */ -export interface ExecutionContext< - InspectorAdapters extends Adapters = Adapters, - ExecutionContextSearch extends SerializableRecord = SerializableRecord -> { +export interface ExecutionContext { /** * Get search context of the expression. */ diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 0480e4966c01f..e73e07a387c46 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -8,7 +8,6 @@ import { Observable } from 'rxjs'; import type { Logger } from '@kbn/logging'; -import type { SerializableRecord } from '@kbn/utility-types'; import type { KibanaRequest } from '@kbn/core/server'; import type { KibanaExecutionContext } from '@kbn/core/public'; @@ -19,6 +18,7 @@ import { VersionedState, } from '@kbn/kibana-utils-plugin/common'; import { Adapters } from '@kbn/inspector-plugin/common/adapters'; +import { ExecutionContextSearch } from '@kbn/es-query'; import { Executor } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; import { ExpressionAstExpression } from '../ast'; @@ -130,7 +130,7 @@ export interface ExpressionsServiceSetup { } export interface ExpressionExecutionParams { - searchContext?: SerializableRecord; + searchContext?: ExecutionContextSearch; variables?: Record; diff --git a/src/plugins/expressions/public/react_expression_renderer/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer/react_expression_renderer.test.tsx index 0afdf72ea9b36..015dac1314bab 100644 --- a/src/plugins/expressions/public/react_expression_renderer/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer/react_expression_renderer.test.tsx @@ -185,7 +185,7 @@ describe('ExpressionRenderer', () => { reload$={refreshSubject} expression="" debounce={1000} - searchContext={{ from: 'now-15m', to: 'now' }} + searchContext={{ timeRange: { from: 'now-15m', to: 'now' } }} /> ); diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 870b44e9bc02c..7bbb486fde390 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import type { SerializableRecord } from '@kbn/utility-types'; import type { KibanaExecutionContext } from '@kbn/core/public'; import { Adapters } from '@kbn/inspector-plugin/public'; +import { ExecutionContextSearch } from '@kbn/es-query'; import { IInterpreterRenderHandlers, ExpressionValue, @@ -35,7 +35,7 @@ export interface ExpressionInterpreter { } export interface IExpressionLoaderParams { - searchContext?: SerializableRecord; + searchContext?: ExecutionContextSearch; context?: ExpressionValue; variables?: Record; // Enables debug tracking on each expression in the AST diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json index 94f1d7852da39..5854100d9fa09 100644 --- a/src/plugins/expressions/tsconfig.json +++ b/src/plugins/expressions/tsconfig.json @@ -17,6 +17,7 @@ "@kbn/core-execution-context-common", "@kbn/tinymath", "@kbn/panel-loader", + "@kbn/es-query", ], "exclude": [ "target/**/*", diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index a7b72cb863f71..f3d7a0e0d1f15 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -202,7 +202,7 @@ describe('kbn_url_storage', () => { await Promise.all([pr1, pr2, pr3]); expect(getCurrentUrl()).toBe('/3'); - expect(urlControls.getPendingUrl()).toBeUndefined(); + expect(urlControls.getPendingUrl()).toEqual(getCurrentUrl()); }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index b81d3c1b81b63..9a4ed8e704b09 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -193,17 +193,17 @@ export const createKbnUrlControls = ( // runs scheduled url updates function flush(replace = shouldReplace) { - const nextUrl = getPendingUrl(); - - if (!nextUrl) return; + if (updateQueue.length === 0) { + return; + } + const nextUrl = getPendingUrl(); cleanUp(); const newUrl = updateUrl(nextUrl, replace); return newUrl; } function getPendingUrl() { - if (updateQueue.length === 0) return undefined; const resultUrl = updateQueue.reduce( (url, nextUpdate) => nextUpdate(url) ?? url, getCurrentUrl(history) diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 6ad69238608c6..d823a95da59ce 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -116,7 +116,11 @@ export const createKbnUrlStateStorage = ( unlisten(); }; }).pipe( - map(() => getStateFromKbnUrl(key, undefined, { getFromHashQuery: useHashQuery })), + map(() => + getStateFromKbnUrl(key, history?.createHref(history.location), { + getFromHashQuery: useHashQuery, + }) + ), catchError((error) => { if (onGetErrorThrottled) onGetErrorThrottled(error); return of(null); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar.scss b/src/plugins/unified_search/public/query_string_input/query_bar.scss index 89cca695ee154..2ecf27416a907 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar.scss +++ b/src/plugins/unified_search/public/query_string_input/query_bar.scss @@ -1,13 +1,9 @@ .kbnQueryBar__datePickerWrapper { .euiDatePopoverButton-isInvalid { background-image: euiFormControlGradient($euiColorDanger); - - // @todo Remove when EUI issue is resolved. - // @see https://github.com/elastic/eui/issues/4612 - &:focus { - color: $euiTextColor; - background-color: $euiFormBackgroundColor; - background-image: euiFormControlGradient($euiColorPrimary); - } } } +// increase the section height to avoid vertical scrolling +.euiQuickSelectPopover__section { + max-height: 142px; +} diff --git a/src/plugins/vis_types/vega/public/vega_fn.ts b/src/plugins/vis_types/vega/public/vega_fn.ts index 6dbd7c9792502..784d4ce1b7026 100644 --- a/src/plugins/vis_types/vega/public/vega_fn.ts +++ b/src/plugins/vis_types/vega/public/vega_fn.ts @@ -8,7 +8,6 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExecutionContextSearch } from '@kbn/data-plugin/public'; import { ExecutionContext, ExpressionFunctionDefinition, @@ -40,7 +39,7 @@ export type VegaExpressionFunctionDefinition = ExpressionFunctionDefinition< Input, Arguments, Output, - ExecutionContext + ExecutionContext >; export const createVegaFn = ( diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client/actions_client.test.ts index 35860bdfb0c53..02c647f17664d 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.test.ts @@ -43,7 +43,7 @@ import { actionsAuthorizationMock } from '../authorization/actions_authorization import { trackLegacyRBACExemption } from '../lib/track_legacy_rbac_exemption'; import { ConnectorTokenClient } from '../lib/connector_token_client'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; -import { Logger } from '@kbn/core/server'; +import { Logger, SavedObject } from '@kbn/core/server'; import { connectorTokenClientMock } from '../lib/connector_token_client.mock'; import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token'; @@ -120,6 +120,14 @@ const executor: ExecutorType<{}, {}, {}, void> = async (options) => { const connectorTokenClient = connectorTokenClientMock.create(); const inMemoryMetrics = inMemoryMetricsMock.create(); +const actionTypeIdFromSavedObjectMock = (actionTypeId: string = 'my-action-type') => { + return { + attributes: { + actionTypeId, + }, + } as SavedObject; +}; + beforeEach(() => { jest.resetAllMocks(); mockedLicenseState = licenseStateMock.create(); @@ -2664,6 +2672,7 @@ describe('execute()', () => { (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { return AuthorizationMode.RBAC; }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock()); await actionsClient.execute({ actionId: 'action-id', params: { @@ -2672,6 +2681,7 @@ describe('execute()', () => { source: asHttpRequestExecutionSource(request), }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + actionTypeId: 'my-action-type', operation: 'execute', additionalPrivileges: [], }); @@ -2685,6 +2695,8 @@ describe('execute()', () => { new Error(`Unauthorized to execute all actions`) ); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock()); + await expect( actionsClient.execute({ actionId: 'action-id', @@ -2696,6 +2708,7 @@ describe('execute()', () => { ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + actionTypeId: 'my-action-type', operation: 'execute', additionalPrivileges: [], }); @@ -2768,12 +2781,15 @@ describe('execute()', () => { executor, }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock()); + await actionsClient.execute({ actionId: 'system-connector-.cases', params: {}, }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + actionTypeId: 'my-action-type', operation: 'execute', additionalPrivileges: ['test/create'], }); @@ -2832,12 +2848,15 @@ describe('execute()', () => { executor, }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock()); + await actionsClient.execute({ actionId: 'testPreconfigured', params: {}, }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + actionTypeId: 'my-action-type', operation: 'execute', additionalPrivileges: [], }); @@ -2895,12 +2914,15 @@ describe('execute()', () => { executor, }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock()); + await actionsClient.execute({ actionId: 'system-connector-.cases', params: { foo: 'bar' }, }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + actionTypeId: 'my-action-type', operation: 'execute', additionalPrivileges: ['test/create'], }); @@ -3032,6 +3054,7 @@ describe('bulkEnqueueExecution()', () => { }, ]); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + actionTypeId: 'my-action-type', operation: 'execute', }); }); diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.ts b/x-pack/plugins/actions/server/actions_client/actions_client.ts index 85376e64897ad..a5314ab2d7d3d 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.ts @@ -11,7 +11,7 @@ import url from 'url'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { i18n } from '@kbn/i18n'; -import { omitBy, isUndefined, compact } from 'lodash'; +import { omitBy, isUndefined, compact, uniq } from 'lodash'; import { IScopedClusterClient, SavedObjectsClientContract, @@ -681,14 +681,39 @@ export class ActionsClient { }: Omit): Promise< ActionTypeExecutorResult > { + const log = this.context.logger; + if ( (await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) === AuthorizationMode.RBAC ) { const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params); + let actionTypeId: string | undefined; + + try { + if (this.isPreconfigured(actionId)) { + const connector = this.context.inMemoryConnectors.find( + (inMemoryConnector) => inMemoryConnector.id === actionId + ); + + actionTypeId = connector?.actionTypeId; + } else { + // TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute + const { attributes } = await this.context.unsecuredSavedObjectsClient.get( + 'action', + actionId + ); + + actionTypeId = attributes.actionTypeId; + } + } catch (err) { + log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err); + } + await this.context.authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges, + actionTypeId, }); } else { trackLegacyRBACExemption('execute', this.context.usageCounter); @@ -723,6 +748,11 @@ export class ActionsClient { * inside the ActionExecutor at execution time */ await this.context.authorization.ensureAuthorized({ operation: 'execute' }); + await Promise.all( + uniq(options.map((o) => o.actionTypeId)).map((actionTypeId) => + this.context.authorization.ensureAuthorized({ operation: 'execute', actionTypeId }) + ) + ); } if (authModes[AuthorizationMode.Legacy] > 0) { trackLegacyRBACExemption( @@ -740,7 +770,10 @@ export class ActionsClient { (await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) === AuthorizationMode.RBAC ) { - await this.context.authorization.ensureAuthorized({ operation: 'execute' }); + await this.context.authorization.ensureAuthorized({ + operation: 'execute', + actionTypeId: options.actionTypeId, + }); } else { trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.context.usageCounter); } diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index fa65b06777f98..f9f2094ba05ba 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -14,10 +14,16 @@ import { } from '../constants/saved_objects'; import { AuthenticatedUser } from '@kbn/security-plugin/server'; import { AuthorizationMode } from './get_authorization_mode_by_source'; +import { + CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG, + CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG, +} from '../feature'; const request = {} as KibanaRequest; const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`; +const BASIC_EXECUTE_AUTHZ = `api:${CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG}`; +const ADVANCED_EXECUTE_AUTHZ = `api:${CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG}`; function mockSecurity() { const security = securityMock.createSetup(); @@ -83,7 +89,7 @@ describe('ensureAuthorized', () => { expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create'); expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('action', 'create')], + kibana: [mockAuthorizationAction('action', 'create'), BASIC_EXECUTE_AUTHZ], }); }); @@ -123,6 +129,7 @@ describe('ensureAuthorized', () => { kibana: [ mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), + BASIC_EXECUTE_AUTHZ, ], }); }); @@ -225,6 +232,54 @@ describe('ensureAuthorized', () => { mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), 'test/create', + BASIC_EXECUTE_AUTHZ, + ], + }); + }); + + test('checks SentinelOne connector privileges correctly', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const actionsAuthorization = new ActionsAuthorization({ + request, + authorization, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'execute'), + authorized: true, + }, + ], + }); + + await actionsAuthorization.ensureAuthorized({ + operation: 'execute', + actionTypeId: '.sentinelone', + }); + + expect(authorization.actions.savedObject.get).toHaveBeenCalledWith( + ACTION_SAVED_OBJECT_TYPE, + 'get' + ); + + expect(authorization.actions.savedObject.get).toHaveBeenCalledWith( + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + 'create' + ); + + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [ + mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), + mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), + ADVANCED_EXECUTE_AUTHZ, ], }); }); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index 34eec819b431b..df282b95e9686 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -74,7 +74,15 @@ export class ActionsAuthorization { : [authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)]; const { hasAllRequested } = await checkPrivileges({ - kibana: [...privileges, ...additionalPrivileges], + kibana: [ + ...privileges, + ...additionalPrivileges, + // SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors. + // This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions + actionTypeId === '.sentinelone' + ? 'api:actions:execute-advanced-connectors' + : 'api:actions:execute-basic-connectors', + ], }); if (!hasAllRequested) { throw Boom.forbidden( diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts index aa0b88dbee633..b44aaf61cad62 100644 --- a/x-pack/plugins/actions/server/feature.ts +++ b/x-pack/plugins/actions/server/feature.ts @@ -13,6 +13,9 @@ import { CONNECTOR_TOKEN_SAVED_OBJECT_TYPE, } from './constants/saved_objects'; +export const CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-advanced-connectors'; +export const CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-basic-connectors'; + /** * The order of appearance in the feature privilege page * under the management section. @@ -33,7 +36,10 @@ export const ACTIONS_FEATURE = { privileges: { all: { app: [], - api: [], + api: [ + CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG, + CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG, + ], catalogue: [], management: { insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'], @@ -50,7 +56,7 @@ export const ACTIONS_FEATURE = { }, read: { app: [], - api: [], + api: [CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG], catalogue: [], management: { insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'], diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 9e277b9084329..c58e16a11aa4a 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -835,6 +835,7 @@ test('successfully authorize system actions', async () => { await actionExecutor.execute({ ...executeParams, actionId: 'system-connector-.cases' }); expect(authorizationMock.ensureAuthorized).toBeCalledWith({ + actionTypeId: '.cases', operation: 'execute', additionalPrivileges: ['test/create'], }); @@ -875,7 +876,10 @@ test('Execute of SentinelOne sub-actions require create privilege', async () => await actionExecutor.execute({ ...executeParams, actionId: 'sentinel-one-connector-authz' }); - expect(authorizationMock.ensureAuthorized).toHaveBeenCalledWith({ operation: 'create' }); + expect(authorizationMock.ensureAuthorized).toHaveBeenCalledWith({ + operation: 'execute', + actionTypeId: '.sentinelone', + }); }); test('pass the params to the actionTypeRegistry when authorizing system actions', async () => { @@ -909,6 +913,7 @@ test('pass the params to the actionTypeRegistry when authorizing system actions' }); expect(authorizationMock.ensureAuthorized).toBeCalledWith({ + actionTypeId: '.cases', operation: 'execute', additionalPrivileges: ['test/create'], }); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index fd1d8f92a0482..69aab56e4e5b3 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -565,14 +565,17 @@ const ensureAuthorizedToExecute = async ({ params ); - await authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges }); - } - - // SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors. - // This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions - if (actionTypeId === '.sentinelone') { await authorization.ensureAuthorized({ - operation: 'create', + operation: 'execute', + additionalPrivileges, + actionTypeId, + }); + } else if (actionTypeId === '.sentinelone') { + // SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors. + // This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions + await authorization.ensureAuthorized({ + operation: 'execute', + actionTypeId, }); } } catch (error) { 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 79ecdc077ea6f..a372355d1db7c 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 @@ -151,6 +151,12 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "description": "Total number of services utilizing the opentelemetry/swift agent within the last day" } }, + "opentelemetry/android": { + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/android agent within the last day" + } + }, "opentelemetry/webjs": { "type": "long", "_meta": { 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 e52be12ca5c36..33a95f246f86b 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts @@ -373,6 +373,13 @@ const apmPerAgentSchema: Pick< 'Total number of services utilizing the opentelemetry/swift agent within the last day', }, }, + 'opentelemetry/android': { + type: 'long', + _meta: { + description: + 'Total number of services utilizing the opentelemetry/android agent within the last day', + }, + }, 'opentelemetry/webjs': { type: 'long', _meta: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts index a3941afc07e44..4442b1afe84d0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SerializableRecord } from '@kbn/utility-types'; import { functionWrapper } from '@kbn/presentation-util-plugin/test_helpers'; import { getFunctionErrors } from '../../../i18n'; import { csv } from './csv'; @@ -40,7 +39,7 @@ one,1 two,2 fourty two,42`, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -56,7 +55,7 @@ two\t2 fourty two\t42`, delimiter: '\t', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); @@ -70,7 +69,7 @@ two%SPLIT%2 fourty two%SPLIT%42`, delimiter: '%SPLIT%', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -83,7 +82,7 @@ fourty two%SPLIT%42`, data: `name,number\rone,1\rtwo,2\rfourty two,42`, newline: '\r', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -107,7 +106,7 @@ fourty two%SPLIT%42`, data: `foo," bar ", baz, " buz " 1,2,3,4`, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expectedResult); }); @@ -135,7 +134,7 @@ fourty two%SPLIT%42`, 1," best ",3, " ok" " good", bad, better , " worst " `, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expectedResult); }); @@ -150,7 +149,7 @@ one|1 two.2 fourty two,42`, }, - {} as ExecutionContext + {} as ExecutionContext ); }).toThrow(new RegExp(errors.invalidInputCSV().message)); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts index c3cf677c87ff6..8c500bd28c1ca 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts @@ -10,25 +10,16 @@ import { testTable, relationalTable } from './__fixtures__/test_tables'; import { dropdownControl } from './dropdownControl'; import { ExecutionContext } from '@kbn/expressions-plugin/common'; import { Adapters } from '@kbn/inspector-plugin/common'; -import { SerializableRecord } from '@kbn/utility-types'; describe('dropdownControl', () => { const fn = functionWrapper(dropdownControl); it('returns a render as dropdown_filter', () => { expect( - fn( - testTable, - { filterColumn: 'name', valueColumn: 'name' }, - {} as ExecutionContext - ) + fn(testTable, { filterColumn: 'name', valueColumn: 'name' }, {} as ExecutionContext) ).toHaveProperty('type', 'render'); expect( - fn( - testTable, - { filterColumn: 'name', valueColumn: 'name' }, - {} as ExecutionContext - ) + fn(testTable, { filterColumn: 'name', valueColumn: 'name' }, {} as ExecutionContext) ).toHaveProperty('as', 'dropdown_filter'); }); @@ -41,25 +32,16 @@ describe('dropdownControl', () => { [] ); expect( - fn( - testTable, - { valueColumn: 'name' }, - {} as ExecutionContext - )?.value?.choices + fn(testTable, { valueColumn: 'name' }, {} as ExecutionContext)?.value?.choices ).toEqual(uniqueNames); }); it('returns an empty array when provided an invalid column', () => { expect( - fn( - testTable, - { valueColumn: 'foo' }, - {} as ExecutionContext - )?.value?.choices + fn(testTable, { valueColumn: 'foo' }, {} as ExecutionContext)?.value?.choices ).toEqual([]); expect( - fn(testTable, { valueColumn: '' }, {} as ExecutionContext) - ?.value?.choices + fn(testTable, { valueColumn: '' }, {} as ExecutionContext)?.value?.choices ).toEqual([]); }); }); @@ -71,7 +53,7 @@ describe('dropdownControl', () => { fn( relationalTable, { valueColumn: 'id', labelColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext )?.value?.choices ).toEqual(expectedChoices); }); @@ -81,28 +63,20 @@ describe('dropdownControl', () => { describe('filterColumn', () => { it('sets which column the filter is applied to', () => { expect( - fn( - testTable, - { filterColumn: 'name' }, - {} as ExecutionContext - )?.value + fn(testTable, { filterColumn: 'name' }, {} as ExecutionContext)?.value ).toHaveProperty('column', 'name'); expect( fn( testTable, { filterColumn: 'name', valueColumn: 'price' }, - {} as ExecutionContext + {} as ExecutionContext )?.value ).toHaveProperty('column', 'name'); }); it('defaults to valueColumn if not provided', () => { expect( - fn( - testTable, - { valueColumn: 'price' }, - {} as ExecutionContext - )?.value + fn(testTable, { valueColumn: 'price' }, {} as ExecutionContext)?.value ).toHaveProperty('column', 'price'); }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 98f99fbd29245..ef7e2750b34fd 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -52,8 +52,8 @@ export const AccountsEvaluatedWidget = ({ }) => { const { euiTheme } = useEuiTheme(); - const filterBenchmarksById = (benchmarkId: string) => { - return benchmarkAssets?.filter((obj) => obj?.meta.benchmarkId === benchmarkId) || []; + const getBenchmarkById = (benchmarkId: string) => { + return benchmarkAssets?.find((obj) => obj?.meta.benchmarkId === benchmarkId); }; const navToFindings = useNavigateFindings(); @@ -67,7 +67,7 @@ export const AccountsEvaluatedWidget = ({ }; const benchmarkElements = benchmarks.map((benchmark) => { - const cloudAssetAmount = filterBenchmarksById(benchmark.type).length; + const cloudAssetAmount = getBenchmarkById(benchmark.type)?.meta?.assetCount || 0; return ( cloudAssetAmount > 0 && ( diff --git a/x-pack/plugins/dataset_quality/common/api_types.ts b/x-pack/plugins/dataset_quality/common/api_types.ts index 88a7af594b207..4f9936d65e014 100644 --- a/x-pack/plugins/dataset_quality/common/api_types.ts +++ b/x-pack/plugins/dataset_quality/common/api_types.ts @@ -42,12 +42,12 @@ export const integrationRt = rt.intersection([ }), ]); -export const malformedDocsRt = rt.type({ +export const degradedDocsRt = rt.type({ dataset: rt.string, percentage: rt.number, }); -export type MalformedDocs = rt.TypeOf; +export type DegradedDocs = rt.TypeOf; export const getDataStreamsStatsResponseRt = rt.exact( rt.intersection([ @@ -60,8 +60,8 @@ export const getDataStreamsStatsResponseRt = rt.exact( ]) ); -export const getDataStreamsMalformedDocsStatsResponseRt = rt.exact( +export const getDataStreamsDegradedDocsStatsResponseRt = rt.exact( rt.type({ - malformedDocs: rt.array(malformedDocsRt), + degradedDocs: rt.array(degradedDocsRt), }) ); diff --git a/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts b/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts index 96035415b3649..2456284eb3cdc 100644 --- a/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts +++ b/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_stat.ts @@ -15,7 +15,7 @@ export class DataStreamStat { sizeBytes?: DataStreamStatType['size_bytes']; lastActivity?: DataStreamStatType['last_activity']; integration?: IntegrationType; - malformedDocs?: number; + degradedDocs?: number; private constructor(dataStreamStat: DataStreamStat) { this.name = dataStreamStat.name; @@ -24,7 +24,7 @@ export class DataStreamStat { this.sizeBytes = dataStreamStat.sizeBytes; this.lastActivity = dataStreamStat.lastActivity; this.integration = dataStreamStat.integration; - this.malformedDocs = dataStreamStat.malformedDocs; + this.degradedDocs = dataStreamStat.degradedDocs; } public static create(dataStreamStat: DataStreamStatType) { diff --git a/x-pack/plugins/dataset_quality/common/data_streams_stats/malformed_docs_stat.ts b/x-pack/plugins/dataset_quality/common/data_streams_stats/malformed_docs_stat.ts index d48e4452b548f..b198f45be7101 100644 --- a/x-pack/plugins/dataset_quality/common/data_streams_stats/malformed_docs_stat.ts +++ b/x-pack/plugins/dataset_quality/common/data_streams_stats/malformed_docs_stat.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { MalformedDocsStatType } from './types'; +import { DegradedDocsStatType } from './types'; -export class MalformedDocsStat { - dataset: MalformedDocsStatType['dataset']; - percentage: MalformedDocsStatType['percentage']; +export class DegradedDocsStat { + dataset: DegradedDocsStatType['dataset']; + percentage: DegradedDocsStatType['percentage']; - private constructor(malformedDocsStat: MalformedDocsStat) { - this.dataset = malformedDocsStat.dataset; - this.percentage = malformedDocsStat.percentage; + private constructor(degradedDocsStat: DegradedDocsStat) { + this.dataset = degradedDocsStat.dataset; + this.percentage = degradedDocsStat.percentage; } - public static create(malformedDocsStat: MalformedDocsStatType) { - return new MalformedDocsStat(malformedDocsStat); + public static create(degradedDocsStat: DegradedDocsStatType) { + return new DegradedDocsStat(degradedDocsStat); } } diff --git a/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts b/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts index 586732f0899c9..1db25258a04f8 100644 --- a/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts +++ b/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts @@ -19,10 +19,10 @@ export type DataStreamStatType = GetDataStreamsStatsResponse['dataStreamsStats'] integration?: IntegrationType; }; -export type GetDataStreamsMalformedDocsStatsParams = - APIClientRequestParamsOf<`GET /internal/dataset_quality/data_streams/malformed_docs`>['params']; -export type GetDataStreamsMalformedDocsStatsQuery = GetDataStreamsMalformedDocsStatsParams['query']; -export type GetDataStreamsMalformedDocsStatsResponse = - APIReturnType<`GET /internal/dataset_quality/data_streams/malformed_docs`>; -export type DataStreamMalformedDocsStatServiceResponse = MalformedDocsStatType[]; -export type MalformedDocsStatType = GetDataStreamsMalformedDocsStatsResponse['malformedDocs'][0]; +export type GetDataStreamsDegradedDocsStatsParams = + APIClientRequestParamsOf<`GET /internal/dataset_quality/data_streams/degraded_docs`>['params']; +export type GetDataStreamsDegradedDocsStatsQuery = GetDataStreamsDegradedDocsStatsParams['query']; +export type GetDataStreamsDegradedDocsStatsResponse = + APIReturnType<`GET /internal/dataset_quality/data_streams/degraded_docs`>; +export type DataStreamDegradedDocsStatServiceResponse = DegradedDocsStatType[]; +export type DegradedDocsStatType = GetDataStreamsDegradedDocsStatsResponse['degradedDocs'][0]; diff --git a/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx b/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx index cccd4d3b12bb7..94223e10f316f 100644 --- a/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx +++ b/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx @@ -37,20 +37,20 @@ const sizeColumnName = i18n.translate('xpack.datasetQuality.sizeColumnName', { defaultMessage: 'Size', }); -const malformedDocsColumnName = i18n.translate('xpack.datasetQuality.malformedDocsColumnName', { - defaultMessage: 'Malformed Docs', +const degradedDocsColumnName = i18n.translate('xpack.datasetQuality.degradedDocsColumnName', { + defaultMessage: 'Degraded Docs', }); -const malformedDocsDescription = (minimimPercentage: number) => - i18n.translate('xpack.datasetQuality.malformedDocsQualityDescription', { +const degradedDocsDescription = (minimimPercentage: number) => + i18n.translate('xpack.datasetQuality.degradedDocsQualityDescription', { defaultMessage: 'greater than {minimimPercentage}%', values: { minimimPercentage }, }); -const malformedDocsColumnTooltip = ( +const degradedDocsColumnTooltip = ( @@ -62,13 +62,13 @@ const malformedDocsColumnTooltip = ( - {` ${malformedDocsDescription(POOR_QUALITY_MINIMUM_PERCENTAGE)}`} + {` ${degradedDocsDescription(POOR_QUALITY_MINIMUM_PERCENTAGE)}`} - {` ${malformedDocsDescription(DEGRADED_QUALITY_MINIMUM_PERCENTAGE)}`} + {` ${degradedDocsDescription(DEGRADED_QUALITY_MINIMUM_PERCENTAGE)}`} @@ -89,10 +89,10 @@ const lastActivityColumnName = i18n.translate('xpack.datasetQuality.lastActivity export const getDatasetQualitTableColumns = ({ fieldFormats, - loadingMalformedStats, + loadingDegradedStats, }: { fieldFormats: FieldFormatsStart; - loadingMalformedStats?: boolean; + loadingDegradedStats?: boolean; }): Array> => { return [ { @@ -129,28 +129,28 @@ export const getDatasetQualitTableColumns = ({ }, { name: ( - + - {`${malformedDocsColumnName} `} + {`${degradedDocsColumnName} `} ), - field: 'malformedDocs', + field: 'degradedDocs', sortable: true, render: (_, dataStreamStat: DataStreamStat) => ( - + - {`${dataStreamStat.malformedDocs}%`} + {`${dataStreamStat.degradedDocs ?? 0}%`} ), diff --git a/x-pack/plugins/dataset_quality/public/components/quality_indicator/percentage_indicator.tsx b/x-pack/plugins/dataset_quality/public/components/quality_indicator/percentage_indicator.tsx index 5f446a0367c3c..b3f9d8e3b852c 100644 --- a/x-pack/plugins/dataset_quality/public/components/quality_indicator/percentage_indicator.tsx +++ b/x-pack/plugins/dataset_quality/public/components/quality_indicator/percentage_indicator.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { isNil } from 'lodash'; import React from 'react'; import { DEGRADED_QUALITY_MINIMUM_PERCENTAGE, @@ -13,11 +12,7 @@ import { } from '../../../common/constants'; import { QualityIndicator } from './indicator'; -export function QualityPercentageIndicator({ percentage }: { percentage?: number }) { - if (isNil(percentage)) { - return <>; - } - +export function QualityPercentageIndicator({ percentage = 0 }: { percentage?: number }) { const quality = percentage > POOR_QUALITY_MINIMUM_PERCENTAGE ? 'poor' diff --git a/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx b/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx index bf7afdc9d95e2..4bff4f54f0db3 100644 --- a/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx +++ b/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx @@ -36,9 +36,9 @@ export const useDatasetQualityTable = () => { const { dataStreamsStatsServiceClient: client } = useDatasetQualityContext(); const { data = [], loading } = useFetcher(async () => client.getDataStreamsStats(), []); - const { data: malformedStats = [], loading: loadingMalformedStats } = useFetcher( + const { data: degradedStats = [], loading: loadingDegradedStats } = useFetcher( async () => - client.getDataStreamsMalformedStats({ + client.getDataStreamsDegradedStats({ start: defaultTimeRange.from, end: defaultTimeRange.to, }), @@ -46,8 +46,8 @@ export const useDatasetQualityTable = () => { ); const columns = useMemo( - () => getDatasetQualitTableColumns({ fieldFormats, loadingMalformedStats }), - [fieldFormats, loadingMalformedStats] + () => getDatasetQualitTableColumns({ fieldFormats, loadingDegradedStats }), + [fieldFormats, loadingDegradedStats] ); const pagination = { @@ -77,18 +77,18 @@ export const useDatasetQualityTable = () => { const renderedItems = useMemo(() => { const overridenSortingField = sortingOverrides[sortField] || sortField; const mergedData = data.map((dataStream) => { - const malformedDocs = find(malformedStats, { dataset: dataStream.name }); + const degradedDocs = find(degradedStats, { dataset: dataStream.name }); return { ...dataStream, - malformedDocs: malformedDocs?.percentage, + degradedDocs: degradedDocs?.percentage, }; }); const sortedItems = orderBy(mergedData, overridenSortingField, sortDirection); return sortedItems.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); - }, [data, malformedStats, sortField, sortDirection, pageIndex, pageSize]); + }, [data, degradedStats, sortField, sortDirection, pageIndex, pageSize]); const resultsCount = useMemo(() => { const startNumberItemsOnPage = pageSize * pageIndex + (renderedItems.length ? 1 : 0); diff --git a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts index 21a4b7b7dc2ff..5fe6f9ab28231 100644 --- a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts +++ b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts @@ -9,13 +9,13 @@ import { HttpStart } from '@kbn/core/public'; import { decodeOrThrow } from '@kbn/io-ts-utils'; import { find, merge } from 'lodash'; import { - getDataStreamsMalformedDocsStatsResponseRt, + getDataStreamsDegradedDocsStatsResponseRt, getDataStreamsStatsResponseRt, } from '../../../common/api_types'; import { DataStreamStatServiceResponse, - GetDataStreamsMalformedDocsStatsQuery, - GetDataStreamsMalformedDocsStatsResponse, + GetDataStreamsDegradedDocsStatsQuery, + GetDataStreamsDegradedDocsStatsResponse, GetDataStreamsStatsError, GetDataStreamsStatsQuery, GetDataStreamsStatsResponse, @@ -52,10 +52,10 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { return mergedDataStreamsStats.map(DataStreamStat.create); } - public async getDataStreamsMalformedStats(params: GetDataStreamsMalformedDocsStatsQuery) { + public async getDataStreamsDegradedStats(params: GetDataStreamsDegradedDocsStatsQuery) { const response = await this.http - .get( - '/internal/dataset_quality/data_streams/malformed_docs', + .get( + '/internal/dataset_quality/data_streams/degraded_docs', { query: { ...params, @@ -65,18 +65,18 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { ) .catch((error) => { throw new GetDataStreamsStatsError( - `Failed to fetch data streams malformed stats": ${error}` + `Failed to fetch data streams degraded stats": ${error}` ); }); - const { malformedDocs } = decodeOrThrow( - getDataStreamsMalformedDocsStatsResponseRt, + const { degradedDocs } = decodeOrThrow( + getDataStreamsDegradedDocsStatsResponseRt, (message: string) => new GetDataStreamsStatsError( - `Failed to decode data streams malformed docs stats response: ${message}"` + `Failed to decode data streams degraded docs stats response: ${message}"` ) )(response); - return malformedDocs; + return degradedDocs; } } diff --git a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts index 4f79753bc99cd..46c39082ce1d4 100644 --- a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts +++ b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts @@ -7,9 +7,9 @@ import { HttpStart } from '@kbn/core/public'; import { - DataStreamMalformedDocsStatServiceResponse, + DataStreamDegradedDocsStatServiceResponse, DataStreamStatServiceResponse, - GetDataStreamsMalformedDocsStatsQuery, + GetDataStreamsDegradedDocsStatsQuery, GetDataStreamsStatsQuery, } from '../../../common/data_streams_stats'; @@ -25,7 +25,7 @@ export interface DataStreamsStatsServiceStartDeps { export interface IDataStreamsStatsClient { getDataStreamsStats(params?: GetDataStreamsStatsQuery): Promise; - getDataStreamsMalformedStats( - params?: GetDataStreamsMalformedDocsStatsQuery - ): Promise; + getDataStreamsDegradedStats( + params?: GetDataStreamsDegradedDocsStatsQuery + ): Promise; } diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_malformed_docs.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_degraded_docs.ts similarity index 83% rename from x-pack/plugins/dataset_quality/server/routes/data_streams/get_malformed_docs.ts rename to x-pack/plugins/dataset_quality/server/routes/data_streams/get_degraded_docs.ts index 155fe981428c7..4bbe01d219322 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_malformed_docs.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_degraded_docs.ts @@ -7,7 +7,7 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; -import { MalformedDocs } from '../../../common/api_types'; +import { DegradedDocs } from '../../../common/api_types'; import { DATA_STREAM_DATASET, DATA_STREAM_NAMESPACE, @@ -17,7 +17,7 @@ import { import { DataStreamTypes } from '../../types/data_stream'; import { createDatasetQualityESClient, wildcardQuery } from '../../utils'; -export async function getMalformedDocsPaginated(options: { +export async function getDegradedDocsPaginated(options: { esClient: ElasticsearchClient; type?: DataStreamTypes; start: number; @@ -27,8 +27,8 @@ export async function getMalformedDocsPaginated(options: { dataset: string; namespace: string; }; - prevResults?: MalformedDocs[]; -}): Promise { + prevResults?: DegradedDocs[]; +}): Promise { const { esClient, type = 'logs', datasetQuery, start, end, after, prevResults = [] } = options; const datasetQualityESClient = createDatasetQualityESClient(esClient); @@ -61,7 +61,7 @@ export async function getMalformedDocsPaginated(options: { ], }, aggs: { - malformed: { + degraded: { filter: { exists: { field: _IGNORED, @@ -73,16 +73,16 @@ export async function getMalformedDocsPaginated(options: { }, }); - const currMalformedDocs = + const currDegradedDocs = response.aggregations?.datasets.buckets.map((bucket) => ({ dataset: `${type}-${bucket.key.dataset}-${bucket.key.namespace}`, - percentage: (bucket.malformed.doc_count * 100) / bucket.doc_count, + percentage: (bucket.degraded.doc_count * 100) / bucket.doc_count, })) ?? []; - const malformedDocs = [...prevResults, ...currMalformedDocs]; + const degradedDocs = [...prevResults, ...currDegradedDocs]; if (response.aggregations?.datasets.after_key) { - return getMalformedDocsPaginated({ + return getDegradedDocsPaginated({ esClient, type, start, @@ -92,9 +92,9 @@ export async function getMalformedDocsPaginated(options: { dataset: response.aggregations?.datasets.after_key.dataset as string, namespace: response.aggregations?.datasets.after_key.namespace as string, }, - prevResults: malformedDocs, + prevResults: degradedDocs, }); } - return malformedDocs; + return degradedDocs; } diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts index a70bcc133e100..7be8c102060d6 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts @@ -13,8 +13,8 @@ import { Integration } from '../../types/integration'; import { createDatasetQualityServerRoute } from '../create_datasets_quality_server_route'; import { getDataStreams } from './get_data_streams'; import { getDataStreamsStats } from './get_data_streams_stats'; -import { getMalformedDocsPaginated } from './get_malformed_docs'; -import { MalformedDocs } from '../../../common/api_types'; +import { getDegradedDocsPaginated } from './get_degraded_docs'; +import { DegradedDocs } from '../../../common/api_types'; const statsRoute = createDatasetQualityServerRoute({ endpoint: 'GET /internal/dataset_quality/data_streams/stats', @@ -72,8 +72,8 @@ const statsRoute = createDatasetQualityServerRoute({ }, }); -const malformedDocsRoute = createDatasetQualityServerRoute({ - endpoint: 'GET /internal/dataset_quality/data_streams/malformed_docs', +const degradedDocsRoute = createDatasetQualityServerRoute({ + endpoint: 'GET /internal/dataset_quality/data_streams/degraded_docs', params: t.type({ query: t.intersection([ rangeRt, @@ -87,25 +87,25 @@ const malformedDocsRoute = createDatasetQualityServerRoute({ tags: [], }, async handler(resources): Promise<{ - malformedDocs: MalformedDocs[]; + degradedDocs: DegradedDocs[]; }> { const { context, params } = resources; const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asCurrentUser; - const malformedDocs = await getMalformedDocsPaginated({ + const degradedDocs = await getDegradedDocsPaginated({ esClient, ...params.query, }); return { - malformedDocs, + degradedDocs, }; }, }); export const dataStreamsRouteRepository = { ...statsRoute, - ...malformedDocsRoute, + ...degradedDocsRoute, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx index 970b1488010c0..62ac53004a773 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx @@ -11,11 +11,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiButtonIcon, EuiPanel, EuiTextColor, EuiTitle } from '@elastic/eui'; +import { EuiButtonEmpty, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; import { InferencePipeline, TrainedModelState } from '../../../../../../common/types/pipelines'; -import { InferencePipelineCard } from './inference_pipeline_card'; +import { InferencePipelineCard, TrainedModelHealthPopover } from './inference_pipeline_card'; import { MLModelTypeBadge } from './ml_model_type_badge'; export const DEFAULT_VALUES: InferencePipeline = { @@ -40,7 +40,7 @@ describe('InferencePipelineCard', () => { it('renders pipeline as title', () => { const wrapper = shallow(); expect(wrapper.find(EuiTitle)).toHaveLength(1); - const title = wrapper.find(EuiTitle).dive(); + const title = wrapper.find(EuiTitle).at(0).children(); expect(title.text()).toBe(DEFAULT_VALUES.pipelineName); }); it('renders pipeline as title with unknown model type', () => { @@ -51,14 +51,14 @@ describe('InferencePipelineCard', () => { const wrapper = shallow(); expect(wrapper.find(EuiTitle)).toHaveLength(1); // does not render subtitle - expect(wrapper.find(EuiTextColor)).toHaveLength(0); - const title = wrapper.find(EuiTitle).dive(); + expect(wrapper.find(EuiText)).toHaveLength(0); + const title = wrapper.find(EuiTitle).at(0).children(); expect(title.text()).toBe(DEFAULT_VALUES.pipelineName); }); it('renders model ID as subtitle', () => { const wrapper = shallow(); - expect(wrapper.find(EuiTextColor)).toHaveLength(1); - const subtitle = wrapper.find(EuiTextColor).dive(); + expect(wrapper.find(EuiText)).toHaveLength(1); + const subtitle = wrapper.find(EuiText).at(0).children(); expect(subtitle.text()).toBe(DEFAULT_VALUES.modelId); }); it('renders model type as badge', () => { @@ -67,20 +67,28 @@ describe('InferencePipelineCard', () => { const badge = wrapper.find(MLModelTypeBadge).render(); expect(badge.text()).toBe('ner'); }); - it('renders fix button when model not deployed', () => { +}); + +describe('TrainedModelHealthPopover', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(mockValues); + }); + it('popover renders fix button when model not deployed', () => { const values = { ...DEFAULT_VALUES, modelState: TrainedModelState.NotDeployed, }; - const wrapper = shallow(); - expect(wrapper.find(EuiButtonIcon)).toHaveLength(1); + const wrapper = shallow(); + expect(wrapper.find(EuiButtonEmpty)).toHaveLength(3); - const fixButton = wrapper.find(EuiButtonIcon); + const fixButton = wrapper.find(EuiButtonEmpty).at(0); expect(fixButton.prop('iconType')).toBe('wrench'); expect(fixButton.prop('href')).toBe('/app/ml/trained_models'); + expect(fixButton.children().text()).toBe('Fix issue in Trained Models'); }); - it('does not render fix button when model deployed', () => { - const wrapper = shallow(); - expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); + it('popover does not render fix button when model deployed', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonEmpty)).toHaveLength(2); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx index dcc4b2a0cad8f..342d05c398878 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx @@ -11,16 +11,14 @@ import { useActions, useValues } from 'kea'; import { EuiButtonEmpty, - EuiButtonIcon, EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiPopover, EuiText, - EuiTextColor, EuiTitle, - EuiToolTip, + useIsWithinMaxBreakpoint, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -39,20 +37,18 @@ import { TrainedModelHealth } from './ml_model_health'; import { MLModelTypeBadge } from './ml_model_type_badge'; import { PipelinesLogic } from './pipelines_logic'; -export const InferencePipelineCard: React.FC = (pipeline) => { +export const TrainedModelHealthPopover: React.FC = (pipeline) => { const { http } = useValues(HttpLogic); const { indexName } = useValues(IndexNameLogic); const { ingestionMethod } = useValues(IndexViewLogic); + + const { deleteMlPipeline, detachMlPipeline } = useActions(PipelinesLogic); + const [isPopOverOpen, setIsPopOverOpen] = useState(false); const [showConfirmDelete, setShowConfirmDelete] = useState(false); - const { deleteMlPipeline, detachMlPipeline } = useActions(PipelinesLogic); - const showConfirmDeleteModal = () => { - setShowConfirmDelete(true); - setIsPopOverOpen(false); - }; - const { modelId, pipelineName, types: modelTypes } = pipeline; - const modelType = getMLType(modelTypes); - const modelTitle = getModelDisplayTitle(modelType); + + const { pipelineName } = pipeline; + const actionButton = ( = (pipeline) => ); + const showConfirmDeleteModal = () => { + setShowConfirmDelete(true); + setIsPopOverOpen(false); + }; + return ( - - - - + <> + setIsPopOverOpen(false)} + > + + {pipeline.modelState === TrainedModelState.NotDeployed && ( - - - -

{pipelineName ?? modelTitle}

-
-
- -
-
- {modelTitle && ( - - - - {modelId} - - - - - - - - - )} -
-
- - setIsPopOverOpen(false)} - > - {pipeline.modelState === TrainedModelState.NotDeployed && ( - - + + {i18n.translate( 'xpack.enterpriseSearch.inferencePipelineCard.modelState.notDeployed.fixLink', - { defaultMessage: 'Fix issue in Trained Models' } + { + defaultMessage: 'Fix issue in Trained Models', + } )} - > - - - - )} - - -
- - {i18n.translate('xpack.enterpriseSearch.inferencePipelineCard.action.view', { - defaultMessage: 'View in Stack Management', - })} - -
-
- -
- { - detachMlPipeline({ indexName, pipelineName }); - setIsPopOverOpen(false); - }} - > - {i18n.translate('xpack.enterpriseSearch.inferencePipelineCard.action.detach', { - defaultMessage: 'Detach pipeline', - })} - -
-
- -
- -
-
-
-
-
-
+ + +
+ )} + + + + {i18n.translate('xpack.enterpriseSearch.inferencePipelineCard.action.view', { + defaultMessage: 'View in Stack Management', + })} + + + + + + { + detachMlPipeline({ indexName, pipelineName }); + setIsPopOverOpen(false); + }} + > + {i18n.translate('xpack.enterpriseSearch.inferencePipelineCard.action.detach', { + defaultMessage: 'Detach pipeline', + })} + + + + + + + + + + {showConfirmDelete && ( = (pipeline) => )} + + ); +}; + +export const InferencePipelineCard: React.FC = (pipeline) => { + const { modelId, pipelineName, types: modelTypes } = pipeline; + const modelType = getMLType(modelTypes); + const modelTitle = getModelDisplayTitle(modelType); + const isSmallScreen = useIsWithinMaxBreakpoint('s'); + + return ( + + + + + + +

{pipelineName ?? modelTitle}

+
+
+ {modelTitle && ( + + + + + {modelId} + + + + + + + + + + )} +
+
+ + + +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx index deea940cfe68d..49e00f30c12d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx @@ -45,21 +45,21 @@ export const PipelineSelectOption: React.FC = ({ pipe // TODO: Add model state & pipeline info link. Make sure to check mobile rendering when doing this! - -
{pipeline.pipelineName}
+ +

{pipeline.pipelineName}

- + {modelIdDisplay} {pipeline.modelType.length > 0 && ( - {/* Wrap in a div to prevent the badge from growing to a whole row on mobile*/} -
+ {/* Wrap in a span to prevent the badge from growing to a whole row on mobile*/} + -
+
)}
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.test.ts index 0d7fd264b143f..08c82e8523e44 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.test.ts @@ -19,12 +19,15 @@ jest.mock('@kbn/search-index-documents/lib', () => ({ describe('Elasticsearch Search', () => { let mockRouter: MockRouter; - const mockClient = {}; - + const mockClient = { + asCurrentUser: jest.fn(), + }; beforeEach(() => { const context = { - core: Promise.resolve({ elasticsearch: { client: mockClient } }), - } as jest.Mocked; + core: Promise.resolve({ + elasticsearch: { client: mockClient }, + }), + } as unknown as jest.Mocked; mockRouter = new MockRouter({ context, @@ -90,7 +93,7 @@ describe('Elasticsearch Search', () => { beforeEach(() => { const context = { core: Promise.resolve({ elasticsearch: { client: mockClient } }), - } as jest.Mocked; + } as unknown as jest.Mocked; mockRouterNoQuery = new MockRouter({ context, @@ -137,7 +140,7 @@ describe('Elasticsearch Search', () => { }); expect(fetchSearchResults).toHaveBeenCalledWith( - mockClient, + mockClient.asCurrentUser, 'search-index-name', 'banana', 0, diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.ts index f1f6862c58a8f..430740f85644c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/search.ts @@ -43,7 +43,7 @@ export function registerSearchRoute({ router, log }: RouteDependencies) { elasticsearchErrorHandler(log, async (context, request, response) => { const indexName = decodeURIComponent(request.params.index_name); const searchQuery = request.body.searchQuery; - const { client } = (await context.core).elasticsearch; + const client = (await context.core).elasticsearch.client.asCurrentUser; const { page = 0, size = ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } = request.query; const from = page * size; try { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts index 773ed7273d885..32d0c9e23fb78 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/retry.ts @@ -17,7 +17,10 @@ const retryResponseStatuses = [ 410, // Gone ]; +const retryMessages = ['no_shard_available_action_exception']; + const isRetryableError = (e: any, additionalResponseStatuses: number[] = []) => + retryMessages.some((msg) => e.message.includes(msg)) || e instanceof EsErrors.NoLivingConnectionsError || e instanceof EsErrors.ConnectionError || e instanceof EsErrors.TimeoutError || diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index a5c23c6c87914..3f94d11d731e9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -28,7 +28,8 @@ import { IconType } from '@elastic/eui/src/components/icon/icon'; import { Ast, fromExpression, toExpression } from '@kbn/interpreter'; import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; -import { DataPublicPluginStart, ExecutionContextSearch } from '@kbn/data-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { ExecutionContextSearch } from '@kbn/es-query'; import { ReactExpressionRendererProps, ReactExpressionRendererType, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 6af4fe67c0622..73a4ef853390d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -23,7 +23,8 @@ import { EuiSpacer, } from '@elastic/eui'; import type { CoreStart } from '@kbn/core/public'; -import type { DataPublicPluginStart, ExecutionContextSearch } from '@kbn/data-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { ExecutionContextSearch } from '@kbn/es-query'; import type { ExpressionRendererEvent, ExpressionRenderError, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 16d730139c14a..9851a10c8641c 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -20,11 +20,11 @@ import { TimeRange, isOfQueryType, getAggregateQueryMode, + ExecutionContextSearch, } from '@kbn/es-query'; import type { PaletteOutput } from '@kbn/coloring'; import { DataPublicPluginStart, - ExecutionContextSearch, TimefilterContract, FilterManager, getEsQueryConfig, diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index 82205c5b5990e..329d7573c1832 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -13,7 +13,7 @@ import { ReactExpressionRendererType, } from '@kbn/expressions-plugin/public'; import type { CoreStart, KibanaExecutionContext } from '@kbn/core/public'; -import { ExecutionContextSearch } from '@kbn/data-plugin/public'; +import type { ExecutionContextSearch } from '@kbn/es-query'; import { DefaultInspectorAdapters, RenderMode } from '@kbn/expressions-plugin/common'; import classNames from 'classnames'; import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper'; diff --git a/x-pack/plugins/log_explorer/common/constants.ts b/x-pack/plugins/log_explorer/common/constants.ts index 5aeb491b5a9d2..a73f304a76a5f 100644 --- a/x-pack/plugins/log_explorer/common/constants.ts +++ b/x-pack/plugins/log_explorer/common/constants.ts @@ -32,8 +32,17 @@ export const DATA_GRID_COLUMN_WIDTH_SMALL = 240; export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320; // UI preferences -export const DATA_GRID_DEFAULT_COLUMNS = [SERVICE_NAME_FIELD, HOST_NAME_FIELD, MESSAGE_FIELD]; -export const DATA_GRID_COLUMNS_PREFERENCES = { - [HOST_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_MEDIUM }, - [SERVICE_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_SMALL }, -}; +export const DEFAULT_COLUMNS = [ + { + field: SERVICE_NAME_FIELD, + width: DATA_GRID_COLUMN_WIDTH_SMALL, + }, + { + field: HOST_NAME_FIELD, + width: DATA_GRID_COLUMN_WIDTH_MEDIUM, + }, + { + field: MESSAGE_FIELD, + }, +]; +export const DEFAULT_ROWS_PER_PAGE = 100; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts b/x-pack/plugins/log_explorer/common/control_panels/available_control_panels.ts similarity index 69% rename from x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts rename to x-pack/plugins/log_explorer/common/control_panels/available_control_panels.ts index 2a058e2dde017..865b225585486 100644 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts +++ b/x-pack/plugins/log_explorer/common/control_panels/available_control_panels.ts @@ -5,18 +5,13 @@ * 2.0. */ -import { AllDatasetSelection } from '../../../../common/dataset_selection'; -import { ControlPanels, DefaultLogExplorerProfileState } from './types'; - -export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = { - datasetSelection: AllDatasetSelection.create(), -}; - -export const CONTROL_PANELS_URL_KEY = 'controlPanels'; +import { ControlPanels } from './types'; export const availableControlsPanels = { NAMESPACE: 'data_stream.namespace', -}; +} as const; + +export type AvailableControlPanels = typeof availableControlsPanels; export const controlPanelConfigs: ControlPanels = { [availableControlsPanels.NAMESPACE]: { diff --git a/x-pack/plugins/log_explorer/common/control_panels/index.ts b/x-pack/plugins/log_explorer/common/control_panels/index.ts new file mode 100644 index 0000000000000..6ea70a7fd630d --- /dev/null +++ b/x-pack/plugins/log_explorer/common/control_panels/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './available_control_panels'; +export * from './types'; diff --git a/x-pack/plugins/log_explorer/common/control_panels/types.ts b/x-pack/plugins/log_explorer/common/control_panels/types.ts new file mode 100644 index 0000000000000..22381932d34b6 --- /dev/null +++ b/x-pack/plugins/log_explorer/common/control_panels/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as rt from 'io-ts'; + +const PanelRT = rt.type({ + order: rt.number, + width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]), + grow: rt.boolean, + type: rt.string, + explicitInput: rt.intersection([ + rt.type({ id: rt.string }), + rt.partial({ + dataViewId: rt.string, + exclude: rt.boolean, + existsSelected: rt.boolean, + fieldName: rt.string, + selectedOptions: rt.array(rt.string), + title: rt.union([rt.string, rt.undefined]), + }), + ]), +}); + +export const ControlPanelRT = rt.record(rt.string, PanelRT); + +export type ControlPanels = rt.TypeOf; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts b/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts index c505a07da7768..8b8ab4e1ea241 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts @@ -6,7 +6,6 @@ */ import { Dataset } from '../datasets'; -import { encodeDatasetSelection } from './encoding'; import { DatasetSelectionStrategy } from './types'; export class AllDatasetSelection implements DatasetSelectionStrategy { @@ -23,18 +22,13 @@ export class AllDatasetSelection implements DatasetSelectionStrategy { } toDataviewSpec() { - const { name, title } = this.selection.dataset.toDataviewSpec(); - return { - id: this.toURLSelectionId(), - name, - title, - }; + return this.selection.dataset.toDataviewSpec(); } - toURLSelectionId() { - return encodeDatasetSelection({ + toPlainSelection() { + return { selectionType: this.selectionType, - }); + }; } public static create() { diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.test.ts b/x-pack/plugins/log_explorer/common/dataset_selection/encoding.test.ts deleted file mode 100644 index d88d939858d0e..0000000000000 --- a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.test.ts +++ /dev/null @@ -1,100 +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 { IndexPattern } from '@kbn/io-ts-utils'; -import { encodeDatasetSelection, decodeDatasetSelectionId } from './encoding'; -import { DatasetEncodingError } from './errors'; -import { DatasetSelectionPlain } from './types'; - -describe('DatasetSelection', () => { - const allDatasetSelectionPlain: DatasetSelectionPlain = { - selectionType: 'all', - }; - const encodedAllDatasetSelection = 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA'; - - const singleDatasetSelectionPlain: DatasetSelectionPlain = { - selectionType: 'single', - selection: { - name: 'azure', - version: '1.5.23', - dataset: { - name: 'logs-azure.activitylogs-*' as IndexPattern, - title: 'activitylogs', - }, - }, - }; - const encodedSingleDatasetSelection = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu0m8wrEEjTkAjBwCsHAEwBmcuvBQeKACqCADmSPJqUVUA=='; - - const invalidDatasetSelectionPlain = { - selectionType: 'single', - selection: { - dataset: { - // Missing mandatory `name` property - title: 'activitylogs', - }, - }, - }; - const invalidCompressedId = 'random'; - const invalidEncodedDatasetSelection = 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4T2QHMoBKIA=='; - - describe('#encodeDatasetSelection', () => { - test('should encode and compress a valid DatasetSelection plain object', () => { - // Encode AllDatasetSelection plain object - expect(encodeDatasetSelection(allDatasetSelectionPlain)).toEqual(encodedAllDatasetSelection); - // Encode SingleDatasetSelection plain object - expect(encodeDatasetSelection(singleDatasetSelectionPlain)).toEqual( - encodedSingleDatasetSelection - ); - }); - - test('should throw a DatasetEncodingError if the input is an invalid DatasetSelection plain object', () => { - const encodingRunner = () => - encodeDatasetSelection(invalidDatasetSelectionPlain as DatasetSelectionPlain); - - expect(encodingRunner).toThrow(DatasetEncodingError); - expect(encodingRunner).toThrow(/^The current dataset selection is invalid/); - }); - }); - - describe('#decodeDatasetSelectionId', () => { - test('should decode and decompress a valid encoded string', () => { - // Decode AllDatasetSelection plain object - expect(decodeDatasetSelectionId(encodedAllDatasetSelection)).toEqual( - allDatasetSelectionPlain - ); - // Decode SingleDatasetSelection plain object - expect(decodeDatasetSelectionId(encodedSingleDatasetSelection)).toEqual( - singleDatasetSelectionPlain - ); - }); - - test('should throw a DatasetEncodingError if the input is an invalid compressed id', () => { - expect(() => decodeDatasetSelectionId(invalidCompressedId)).toThrow( - new DatasetEncodingError('The stored id is not a valid compressed value.') - ); - }); - - test('should throw a DatasetEncodingError if the decompressed value is an invalid DatasetSelection plain object', () => { - const decodingRunner = () => decodeDatasetSelectionId(invalidEncodedDatasetSelection); - - expect(decodingRunner).toThrow(DatasetEncodingError); - expect(decodingRunner).toThrow(/^The current dataset selection is invalid/); - }); - }); - - test('encoding and decoding should restore the original DatasetSelection plain object', () => { - // Encode/Decode AllDatasetSelection plain object - expect(decodeDatasetSelectionId(encodeDatasetSelection(allDatasetSelectionPlain))).toEqual( - allDatasetSelectionPlain - ); - // Encode/Decode SingleDatasetSelection plain object - expect(decodeDatasetSelectionId(encodeDatasetSelection(singleDatasetSelectionPlain))).toEqual( - singleDatasetSelectionPlain - ); - }); -}); diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.ts b/x-pack/plugins/log_explorer/common/dataset_selection/encoding.ts deleted file mode 100644 index 83a7c5357fde5..0000000000000 --- a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.ts +++ /dev/null @@ -1,40 +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 { decode, encode, RisonValue } from '@kbn/rison'; -import * as lz from 'lz-string'; -import { decodeOrThrow } from '../runtime_types'; -import { DatasetEncodingError } from './errors'; -import { DatasetSelectionPlain, datasetSelectionPlainRT } from './types'; - -export const encodeDatasetSelection = (datasetSelectionPlain: DatasetSelectionPlain) => { - const safeDatasetSelection = decodeOrThrow( - datasetSelectionPlainRT, - (message: string) => - new DatasetEncodingError(`The current dataset selection is invalid: ${message}"`) - )(datasetSelectionPlain); - - return lz.compressToBase64(encode(safeDatasetSelection)); -}; - -export const decodeDatasetSelectionId = (datasetSelectionId: string): DatasetSelectionPlain => { - const risonDatasetSelection: RisonValue = lz.decompressFromBase64(datasetSelectionId); - - if (risonDatasetSelection === null || risonDatasetSelection === '') { - throw new DatasetEncodingError('The stored id is not a valid compressed value.'); - } - - const decodedDatasetSelection = decode(risonDatasetSelection); - - const datasetSelection = decodeOrThrow( - datasetSelectionPlainRT, - (message: string) => - new DatasetEncodingError(`The current dataset selection is invalid: ${message}"`) - )(decodedDatasetSelection); - - return datasetSelection; -}; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts b/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts index 43faebc618140..f881e90723e14 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts @@ -13,11 +13,9 @@ import { UnresolvedDatasetSelection } from './unresolved_dataset_selection'; export const hydrateDatasetSelection = (datasetSelection: DatasetSelectionPlain) => { if (datasetSelection.selectionType === 'all') { return AllDatasetSelection.create(); - } - if (datasetSelection.selectionType === 'single') { + } else if (datasetSelection.selectionType === 'single') { return SingleDatasetSelection.fromSelection(datasetSelection.selection); - } - if (datasetSelection.selectionType === 'unresolved') { + } else { return UnresolvedDatasetSelection.fromSelection(datasetSelection.selection); } }; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts index f390f7a89f87c..26fd974d0f0ac 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts @@ -28,7 +28,6 @@ export const isDatasetSelection = (input: any): input is DatasetSelection => { export * from './all_dataset_selection'; export * from './single_dataset_selection'; export * from './unresolved_dataset_selection'; -export * from './encoding'; export * from './errors'; export * from './hydrate_dataset_selection.ts'; export * from './types'; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts b/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts index 21c788579ed70..6667dd55f3abe 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts @@ -6,7 +6,6 @@ */ import { Dataset } from '../datasets'; -import { encodeDatasetSelection } from './encoding'; import { DatasetSelectionStrategy, SingleDatasetSelectionPayload } from './types'; export class SingleDatasetSelection implements DatasetSelectionStrategy { @@ -29,16 +28,11 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy { } toDataviewSpec() { - const { name, title } = this.selection.dataset.toDataviewSpec(); - return { - id: this.toURLSelectionId(), - name, - title, - }; + return this.selection.dataset.toDataviewSpec(); } - toURLSelectionId() { - return encodeDatasetSelection({ + toPlainSelection() { + return { selectionType: this.selectionType, selection: { name: this.selection.name, @@ -46,7 +40,7 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy { version: this.selection.version, dataset: this.selection.dataset.toPlain(), }, - }); + }; } public static fromSelection(selection: SingleDatasetSelectionPayload) { diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/types.ts b/x-pack/plugins/log_explorer/common/dataset_selection/types.ts index 239bbc1108a29..db3638aff6331 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/types.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/types.ts @@ -62,7 +62,11 @@ export type UnresolvedDatasetSelectionPayload = rt.TypeOf< >; export type DatasetSelectionPlain = rt.TypeOf; +export type DataViewSpecWithId = DataViewSpec & { + id: string; +}; + export interface DatasetSelectionStrategy { - toDataviewSpec(): DataViewSpec; - toURLSelectionId(): string; + toDataviewSpec(): DataViewSpecWithId; + toPlainSelection(): DatasetSelectionPlain; } diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts b/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts index acfd5180f0ed3..e534403fab617 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts @@ -6,7 +6,6 @@ */ import { Dataset } from '../datasets'; -import { encodeDatasetSelection } from './encoding'; import { DatasetSelectionStrategy, UnresolvedDatasetSelectionPayload } from './types'; export class UnresolvedDatasetSelection implements DatasetSelectionStrategy { @@ -25,22 +24,17 @@ export class UnresolvedDatasetSelection implements DatasetSelectionStrategy { } toDataviewSpec() { - const { name, title } = this.selection.dataset.toDataviewSpec(); - return { - id: this.toURLSelectionId(), - name, - title, - }; + return this.selection.dataset.toDataviewSpec(); } - toURLSelectionId() { - return encodeDatasetSelection({ + toPlainSelection() { + return { selectionType: this.selectionType, selection: { name: this.selection.name, dataset: this.selection.dataset.toPlain(), }, - }); + }; } public static fromSelection(selection: UnresolvedDatasetSelectionPayload) { diff --git a/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts b/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts index 6c18f62350e41..18545b24754d8 100644 --- a/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts +++ b/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts @@ -6,9 +6,9 @@ */ import { IconType } from '@elastic/eui'; -import { DataViewSpec } from '@kbn/data-views-plugin/common'; import { IndexPattern } from '@kbn/io-ts-utils'; import { TIMESTAMP_FIELD } from '../../constants'; +import { DataViewSpecWithId } from '../../dataset_selection'; import { DatasetId, DatasetType, IntegrationType } from '../types'; type IntegrationBase = Partial>; @@ -49,7 +49,7 @@ export class Dataset { return `${type}-${dataset}-*` as IndexPattern; } - toDataviewSpec(): DataViewSpec { + toDataviewSpec(): DataViewSpecWithId { // Invert the property because the API returns the index pattern as `name` and a readable name as `title` return { id: this.id, diff --git a/x-pack/plugins/log_explorer/common/display_options/index.ts b/x-pack/plugins/log_explorer/common/display_options/index.ts new file mode 100644 index 0000000000000..6cc0ccaa93a6d --- /dev/null +++ b/x-pack/plugins/log_explorer/common/display_options/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 * from './types'; diff --git a/x-pack/plugins/log_explorer/common/display_options/types.ts b/x-pack/plugins/log_explorer/common/display_options/types.ts new file mode 100644 index 0000000000000..b4c482d088a54 --- /dev/null +++ b/x-pack/plugins/log_explorer/common/display_options/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ChartDisplayOptions { + breakdownField: string | null; +} + +export type PartialChartDisplayOptions = Partial; + +export interface GridColumnDisplayOptions { + field: string; + width?: number; +} + +export interface GridRowsDisplayOptions { + rowHeight: number; + rowsPerPage: number; +} + +export type PartialGridRowsDisplayOptions = Partial; + +export interface GridDisplayOptions { + columns: GridColumnDisplayOptions[]; + rows: GridRowsDisplayOptions; +} + +export type PartialGridDisplayOptions = Partial< + Omit & { rows?: PartialGridRowsDisplayOptions } +>; + +export interface DisplayOptions { + grid: GridDisplayOptions; + chart: ChartDisplayOptions; +} + +export interface PartialDisplayOptions { + grid?: PartialGridDisplayOptions; + chart?: PartialChartDisplayOptions; +} diff --git a/x-pack/plugins/log_explorer/common/index.ts b/x-pack/plugins/log_explorer/common/index.ts index 989f981879ac0..5466a00ae0caa 100644 --- a/x-pack/plugins/log_explorer/common/index.ts +++ b/x-pack/plugins/log_explorer/common/index.ts @@ -5,4 +5,28 @@ * 2.0. */ -export { AllDatasetSelection, UnresolvedDatasetSelection } from './dataset_selection'; +export { + availableControlPanelFields, + availableControlsPanels, + controlPanelConfigs, + ControlPanelRT, +} from './control_panels'; +export type { AvailableControlPanels, ControlPanels } from './control_panels'; +export { + AllDatasetSelection, + datasetSelectionPlainRT, + hydrateDatasetSelection, + UnresolvedDatasetSelection, +} from './dataset_selection'; +export type { DatasetSelectionPlain } from './dataset_selection'; +export type { + ChartDisplayOptions, + DisplayOptions, + GridColumnDisplayOptions, + GridDisplayOptions, + GridRowsDisplayOptions, + PartialChartDisplayOptions, + PartialDisplayOptions, + PartialGridDisplayOptions, + PartialGridRowsDisplayOptions, +} from './display_options'; diff --git a/x-pack/plugins/log_explorer/kibana.jsonc b/x-pack/plugins/log_explorer/kibana.jsonc index 71781ca9cada3..9275751ca1898 100644 --- a/x-pack/plugins/log_explorer/kibana.jsonc +++ b/x-pack/plugins/log_explorer/kibana.jsonc @@ -12,16 +12,18 @@ "logExplorer" ], "requiredPlugins": [ + "controls", "data", "dataViews", "discover", + "embeddable", "fieldFormats", "fleet", "kibanaReact", "kibanaUtils", - "controls", - "embeddable", + "navigation", "share", + "unifiedSearch" ], "optionalPlugins": [], "requiredBundles": [], diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx index 8f1da5cbfc18d..8be2f2ec6b03b 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { FlyoutProps, LogDocument } from './types'; +import { LogExplorerFlyoutContentProps, LogDocument } from './types'; import { useDocDetail } from './use_doc_detail'; import { FlyoutHeader } from './flyout_header'; import { FlyoutHighlights } from './flyout_highlights'; @@ -16,7 +16,7 @@ export function FlyoutDetail({ dataView, doc, actions, -}: Pick) { +}: Pick) { const parsedDoc = useDocDetail(doc as LogDocument, { dataView }); return ( diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts index 958cc1a3351f0..d88d0d1c9ad8a 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts @@ -5,55 +5,4 @@ * 2.0. */ -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { FlyoutContentProps } from '@kbn/discover-plugin/public'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; - -export interface FlyoutProps extends FlyoutContentProps { - dataView: DataView; - doc: LogDocument; -} - -export interface LogDocument extends DataTableRecord { - flattened: { - '@timestamp': string; - 'log.level'?: [string]; - message?: [string]; - - 'host.name'?: string; - 'service.name'?: string; - 'trace.id'?: string; - 'agent.name'?: string; - 'orchestrator.cluster.name'?: string; - 'orchestrator.resource.id'?: string; - 'cloud.provider'?: string; - 'cloud.region'?: string; - 'cloud.availability_zone'?: string; - 'cloud.project.id'?: string; - 'cloud.instance.id'?: string; - 'log.file.path'?: string; - 'data_stream.namespace': string; - 'data_stream.dataset': string; - }; -} - -export interface FlyoutDoc { - '@timestamp': string; - 'log.level'?: string; - message?: string; - - 'host.name'?: string; - 'service.name'?: string; - 'trace.id'?: string; - 'agent.name'?: string; - 'orchestrator.cluster.name'?: string; - 'orchestrator.resource.id'?: string; - 'cloud.provider'?: string; - 'cloud.region'?: string; - 'cloud.availability_zone'?: string; - 'cloud.project.id'?: string; - 'cloud.instance.id'?: string; - 'log.file.path'?: string; - 'data_stream.namespace': string; - 'data_stream.dataset': string; -} +export type { FlyoutDoc, LogDocument, LogExplorerFlyoutContentProps } from '../../controller'; diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts index b07802e5fb946..2a6baca186ae3 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts @@ -7,11 +7,11 @@ import { formatFieldValue } from '@kbn/discover-utils'; import * as constants from '../../../common/constants'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; -import { FlyoutDoc, FlyoutProps, LogDocument } from './types'; +import { FlyoutDoc, LogExplorerFlyoutContentProps, LogDocument } from './types'; export function useDocDetail( doc: LogDocument, - { dataView }: Pick + { dataView }: Pick ): FlyoutDoc { const { services } = useKibanaContextForPlugin(); diff --git a/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx b/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx index 57736dd4b96dd..638ca847f6549 100644 --- a/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx +++ b/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx @@ -5,100 +5,43 @@ * 2.0. */ +import type { ScopedHistory } from '@kbn/core-application-browser'; +import type { CoreStart } from '@kbn/core/public'; import React, { useMemo } from 'react'; -import { ScopedHistory } from '@kbn/core-application-browser'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DiscoverAppState } from '@kbn/discover-plugin/public'; -import type { BehaviorSubject } from 'rxjs'; -import { CoreStart } from '@kbn/core/public'; -import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { HIDE_ANNOUNCEMENTS } from '@kbn/discover-utils'; +import type { LogExplorerController } from '../../controller'; import { createLogExplorerProfileCustomizations } from '../../customizations/log_explorer_profile'; -import { createPropertyGetProxy } from '../../utils/proxies'; -import { LogExplorerProfileContext } from '../../state_machines/log_explorer_profile'; import { LogExplorerStartDeps } from '../../types'; -import { LogExplorerCustomizations } from './types'; export interface CreateLogExplorerArgs { core: CoreStart; plugins: LogExplorerStartDeps; } -export interface LogExplorerStateContainer { - appState?: DiscoverAppState; - logExplorerState?: Partial; -} - export interface LogExplorerProps { - customizations?: LogExplorerCustomizations; scopedHistory: ScopedHistory; - state$?: BehaviorSubject; + controller: LogExplorerController; } export const createLogExplorer = ({ core, plugins }: CreateLogExplorerArgs) => { const { - data, discover: { DiscoverContainer }, } = plugins; - const overrideServices = { - data: createDataServiceProxy(data), - uiSettings: createUiSettingsServiceProxy(core.uiSettings), - }; - - return ({ customizations = {}, scopedHistory, state$ }: LogExplorerProps) => { + return ({ scopedHistory, controller }: LogExplorerProps) => { const logExplorerCustomizations = useMemo( - () => [createLogExplorerProfileCustomizations({ core, customizations, plugins, state$ })], - [customizations, state$] + () => [createLogExplorerProfileCustomizations({ controller, core, plugins })], + [controller] ); + const { urlStateStorage, ...overrideServices } = controller.discoverServices; + return ( ); }; }; - -/** - * Create proxy for the data service, in which session service enablement calls - * are no-ops. - */ -const createDataServiceProxy = (data: DataPublicPluginStart) => { - const noOpEnableStorage = () => {}; - - const sessionServiceProxy = createPropertyGetProxy(data.search.session, { - enableStorage: () => noOpEnableStorage, - }); - - const searchServiceProxy = createPropertyGetProxy(data.search, { - session: () => sessionServiceProxy, - }); - - return createPropertyGetProxy(data, { - search: () => searchServiceProxy, - }); -}; -/** - * Create proxy for the uiSettings service, in which settings preferences are overwritten - * with custom values - */ -const createUiSettingsServiceProxy = (uiSettings: IUiSettingsClient) => { - const overrides: Record = { - [HIDE_ANNOUNCEMENTS]: true, - }; - - return createPropertyGetProxy(uiSettings, { - get: - () => - (key, ...args) => { - if (key in overrides) { - return overrides[key]; - } - - return uiSettings.get(key, ...args); - }, - }); -}; diff --git a/x-pack/plugins/log_explorer/public/components/log_explorer/types.ts b/x-pack/plugins/log_explorer/public/components/log_explorer/types.ts deleted file mode 100644 index 2b366cce7c55c..0000000000000 --- a/x-pack/plugins/log_explorer/public/components/log_explorer/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataTableRecord } from '@kbn/discover-utils/types'; - -export type RenderPreviousContent = () => React.ReactNode; - -export interface LogExplorerFlyoutContentProps { - doc: DataTableRecord; -} - -export type FlyoutRenderContent = ( - renderPreviousContent: RenderPreviousContent, - props: LogExplorerFlyoutContentProps -) => React.ReactNode; - -export interface LogExplorerCustomizations { - flyout?: { - renderContent?: FlyoutRenderContent; - }; -} diff --git a/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts b/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts new file mode 100644 index 0000000000000..97f0cb4ff8b23 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/controller_customizations.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 { DataView } from '@kbn/data-views-plugin/common'; +import { FlyoutContentProps as DiscoverFlyoutContentProps } from '@kbn/discover-plugin/public'; +import type { DataTableRecord } from '@kbn/discover-utils/types'; + +export interface LogExplorerCustomizations { + flyout?: { + renderContent?: RenderContentCustomization; + }; +} + +export interface LogExplorerFlyoutContentProps extends DiscoverFlyoutContentProps { + dataView: DataView; + doc: LogDocument; +} + +export interface LogDocument extends DataTableRecord { + flattened: { + '@timestamp': string; + 'log.level'?: [string]; + message?: [string]; + + 'host.name'?: string; + 'service.name'?: string; + 'trace.id'?: string; + 'agent.name'?: string; + 'orchestrator.cluster.name'?: string; + 'orchestrator.resource.id'?: string; + 'cloud.provider'?: string; + 'cloud.region'?: string; + 'cloud.availability_zone'?: string; + 'cloud.project.id'?: string; + 'cloud.instance.id'?: string; + 'log.file.path'?: string; + 'data_stream.namespace': string; + 'data_stream.dataset': string; + }; +} + +export interface FlyoutDoc { + '@timestamp': string; + 'log.level'?: string; + message?: string; + + 'host.name'?: string; + 'service.name'?: string; + 'trace.id'?: string; + 'agent.name'?: string; + 'orchestrator.cluster.name'?: string; + 'orchestrator.resource.id'?: string; + 'cloud.provider'?: string; + 'cloud.region'?: string; + 'cloud.availability_zone'?: string; + 'cloud.project.id'?: string; + 'cloud.instance.id'?: string; + 'log.file.path'?: string; + 'data_stream.namespace': string; + 'data_stream.dataset': string; +} + +export type RenderContentCustomization = ( + renderPreviousContent: RenderPreviousContent +) => (props: Props) => React.ReactNode; + +export type RenderPreviousContent = (props: Props) => React.ReactNode; diff --git a/x-pack/plugins/log_explorer/public/controller/create_controller.ts b/x-pack/plugins/log_explorer/public/controller/create_controller.ts new file mode 100644 index 0000000000000..53260aeb97281 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/create_controller.ts @@ -0,0 +1,99 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { getDevToolsOptions } from '@kbn/xstate-utils'; +import equal from 'fast-deep-equal'; +import { distinctUntilChanged, EMPTY, from, map, shareReplay } from 'rxjs'; +import { interpret } from 'xstate'; +import { DatasetsService } from '../services/datasets'; +import { createLogExplorerControllerStateMachine } from '../state_machines/log_explorer_controller'; +import { LogExplorerStartDeps } from '../types'; +import { LogExplorerCustomizations } from './controller_customizations'; +import { createDataServiceProxy } from './custom_data_service'; +import { createUiSettingsServiceProxy } from './custom_ui_settings_service'; +import { + createDiscoverMemoryHistory, + createMemoryUrlStateStorage, +} from './custom_url_state_storage'; +import { getContextFromPublicState, getPublicStateFromContext } from './public_state'; +import { + LogExplorerController, + LogExplorerDiscoverServices, + LogExplorerPublicStateUpdate, +} from './types'; + +interface Dependencies { + core: CoreStart; + plugins: LogExplorerStartDeps; +} + +type InitialState = LogExplorerPublicStateUpdate; + +export const createLogExplorerControllerFactory = + ({ core, plugins: { data } }: Dependencies) => + async ({ + customizations = {}, + initialState, + }: { + customizations?: LogExplorerCustomizations; + initialState?: InitialState; + }): Promise => { + const datasetsClient = new DatasetsService().start({ + http: core.http, + }).client; + + const customMemoryHistory = createDiscoverMemoryHistory(); + const customMemoryUrlStateStorage = createMemoryUrlStateStorage(customMemoryHistory); + const customUiSettings = createUiSettingsServiceProxy(core.uiSettings); + const customData = createDataServiceProxy({ + data, + http: core.http, + uiSettings: customUiSettings, + }); + const discoverServices: LogExplorerDiscoverServices = { + data: customData, + history: () => customMemoryHistory, + uiSettings: customUiSettings, + filterManager: customData.query.filterManager, + timefilter: customData.query.timefilter.timefilter, + urlStateStorage: customMemoryUrlStateStorage, + }; + + const initialContext = getContextFromPublicState(initialState ?? {}); + + const machine = createLogExplorerControllerStateMachine({ + datasetsClient, + initialContext, + query: discoverServices.data.query, + toasts: core.notifications.toasts, + }); + + const service = interpret(machine, { + devTools: getDevToolsOptions(), + }); + + const logExplorerState$ = from(service).pipe( + map(({ context }) => getPublicStateFromContext(context)), + distinctUntilChanged(equal), + shareReplay(1) + ); + + return { + actions: {}, + customizations, + datasetsClient, + discoverServices, + event$: EMPTY, + service, + state$: logExplorerState$, + stateMachine: machine, + }; + }; + +export type CreateLogExplorerControllerFactory = typeof createLogExplorerControllerFactory; +export type CreateLogExplorerController = ReturnType; diff --git a/x-pack/plugins/log_explorer/public/controller/custom_data_service.ts b/x-pack/plugins/log_explorer/public/controller/custom_data_service.ts new file mode 100644 index 0000000000000..790fbaa04df86 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/custom_data_service.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpStart } from '@kbn/core-http-browser'; +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { DataPublicPluginStart, NowProvider, QueryService } from '@kbn/data-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { createPropertyGetProxy } from '../utils/proxies'; + +/** + * Create proxy for the data service, in which session service enablement calls + * are no-ops. + */ +export const createDataServiceProxy = ({ + data, + http, + uiSettings, +}: { + data: DataPublicPluginStart; + http: HttpStart; + uiSettings: IUiSettingsClient; +}) => { + /** + * search session + */ + const noOpEnableStorage = () => {}; + + const sessionServiceProxy = createPropertyGetProxy(data.search.session, { + enableStorage: () => noOpEnableStorage, + }); + + const searchServiceProxy = createPropertyGetProxy(data.search, { + session: () => sessionServiceProxy, + }); + + /** + * query + */ + const customStorage = new Storage(localStorage); + const customQueryService = new QueryService(); + customQueryService.setup({ + nowProvider: new NowProvider(), + storage: customStorage, + uiSettings, + }); + const customQuery = customQueryService.start({ + http, + storage: customStorage, + uiSettings, + }); + + /** + * combined + */ + return createPropertyGetProxy(data, { + query: () => customQuery, + search: () => searchServiceProxy, + }); +}; diff --git a/x-pack/plugins/log_explorer/public/controller/custom_ui_settings_service.ts b/x-pack/plugins/log_explorer/public/controller/custom_ui_settings_service.ts new file mode 100644 index 0000000000000..bd247b91ba1f5 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/custom_ui_settings_service.ts @@ -0,0 +1,33 @@ +/* + * 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 { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { HIDE_ANNOUNCEMENTS, MODIFY_COLUMNS_ON_SWITCH } from '@kbn/discover-utils'; +import { createPropertyGetProxy } from '../utils/proxies'; + +/** + * Create proxy for the uiSettings service, in which settings preferences are overwritten + * with custom values + */ +export const createUiSettingsServiceProxy = (uiSettings: IUiSettingsClient) => { + const overrides: Record = { + [HIDE_ANNOUNCEMENTS]: true, + [MODIFY_COLUMNS_ON_SWITCH]: false, + }; + + return createPropertyGetProxy(uiSettings, { + get: + () => + (key, ...args) => { + if (key in overrides) { + return overrides[key]; + } + + return uiSettings.get(key, ...args); + }, + }); +}; diff --git a/x-pack/plugins/log_explorer/public/controller/custom_url_state_storage.ts b/x-pack/plugins/log_explorer/public/controller/custom_url_state_storage.ts new file mode 100644 index 0000000000000..bddb162963376 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/custom_url_state_storage.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { createMemoryHistory } from 'history'; +import { LogExplorerDiscoverServices } from './types'; + +type DiscoverHistory = ReturnType; + +/** + * Create a MemoryHistory instance. It is initialized with an application state + * object, because Discover radically resets too much when the URL is "empty". + */ +export const createDiscoverMemoryHistory = (): DiscoverHistory => + createMemoryHistory({ + initialEntries: [{ search: `?_a=()` }], + }); + +/** + * Create a url state storage that's not connected to the real browser location + * to isolate the Discover component from these side-effects. + */ +export const createMemoryUrlStateStorage = (memoryHistory: DiscoverHistory) => + createKbnUrlStateStorage({ + history: memoryHistory, + useHash: false, + useHashQuery: false, + }); diff --git a/x-pack/plugins/log_explorer/public/controller/index.ts b/x-pack/plugins/log_explorer/public/controller/index.ts new file mode 100644 index 0000000000000..8c1ebbc752dbd --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './controller_customizations'; +export * from './create_controller'; +export * from './provider'; +export * from './types'; diff --git a/x-pack/plugins/log_explorer/public/controller/lazy_create_controller.ts b/x-pack/plugins/log_explorer/public/controller/lazy_create_controller.ts new file mode 100644 index 0000000000000..46104b3960940 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/lazy_create_controller.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CreateLogExplorerControllerFactory } from './create_controller'; + +export const createLogExplorerControllerLazyFactory: CreateLogExplorerControllerFactory = + (dependencies) => async (args) => { + const { createLogExplorerControllerFactory } = await import('./create_controller'); + + return createLogExplorerControllerFactory(dependencies)(args); + }; diff --git a/x-pack/plugins/log_explorer/public/controller/provider.ts b/x-pack/plugins/log_explorer/public/controller/provider.ts new file mode 100644 index 0000000000000..a66b03f477cb1 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/provider.ts @@ -0,0 +1,15 @@ +/* + * 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 createContainer from 'constate'; +import type { LogExplorerController } from './types'; + +const useLogExplorerController = ({ controller }: { controller: LogExplorerController }) => + controller; + +export const [LogExplorerControllerProvider, useLogExplorerControllerContext] = + createContainer(useLogExplorerController); diff --git a/x-pack/plugins/log_explorer/public/controller/public_state.ts b/x-pack/plugins/log_explorer/public/controller/public_state.ts new file mode 100644 index 0000000000000..04c12160a8809 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/public_state.ts @@ -0,0 +1,134 @@ +/* + * 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 { + availableControlsPanels, + controlPanelConfigs, + ControlPanels, + hydrateDatasetSelection, +} from '../../common'; +import { + DEFAULT_CONTEXT, + LogExplorerControllerContext, +} from '../state_machines/log_explorer_controller'; +import { + LogExplorerPublicState, + LogExplorerPublicStateUpdate, + OptionsListControlOption, +} from './types'; + +export const getPublicStateFromContext = ( + context: LogExplorerControllerContext +): LogExplorerPublicState => { + return { + chart: context.chart, + datasetSelection: context.datasetSelection.toPlainSelection(), + grid: context.grid, + filters: context.filters, + query: context.query, + refreshInterval: context.refreshInterval, + time: context.time, + controls: getPublicControlsStateFromControlPanels(context.controlPanels), + }; +}; + +export const getContextFromPublicState = ( + publicState: LogExplorerPublicStateUpdate +): LogExplorerControllerContext => ({ + ...DEFAULT_CONTEXT, + chart: { + ...DEFAULT_CONTEXT.chart, + ...publicState.chart, + }, + controlPanels: getControlPanelsFromPublicControlsState(publicState.controls), + datasetSelection: + publicState.datasetSelection != null + ? hydrateDatasetSelection(publicState.datasetSelection) + : DEFAULT_CONTEXT.datasetSelection, + grid: { + ...DEFAULT_CONTEXT.grid, + ...publicState.grid, + rows: { + ...DEFAULT_CONTEXT.grid.rows, + ...publicState.grid?.rows, + }, + }, + filters: publicState.filters ?? DEFAULT_CONTEXT.filters, + query: publicState.query ?? DEFAULT_CONTEXT.query, + refreshInterval: publicState.refreshInterval ?? DEFAULT_CONTEXT.refreshInterval, + time: publicState.time ?? DEFAULT_CONTEXT.time, +}); + +const getPublicControlsStateFromControlPanels = ( + controlPanels: ControlPanels | undefined +): LogExplorerPublicState['controls'] => + controlPanels != null + ? { + ...(availableControlsPanels.NAMESPACE in controlPanels + ? { + [availableControlsPanels.NAMESPACE]: getOptionsListPublicControlStateFromControlPanel( + controlPanels[availableControlsPanels.NAMESPACE] + ), + } + : {}), + } + : {}; + +const getOptionsListPublicControlStateFromControlPanel = ( + optionsListControlPanel: ControlPanels[string] +): OptionsListControlOption => ({ + mode: optionsListControlPanel.explicitInput.exclude ? 'exclude' : 'include', + selection: optionsListControlPanel.explicitInput.existsSelected + ? { type: 'exists' } + : { + type: 'options', + selectedOptions: optionsListControlPanel.explicitInput.selectedOptions ?? [], + }, +}); + +const getControlPanelsFromPublicControlsState = ( + publicControlsState: LogExplorerPublicStateUpdate['controls'] +): ControlPanels => { + if (publicControlsState == null) { + return {}; + } + + const namespacePublicControlState = publicControlsState[availableControlsPanels.NAMESPACE]; + + return { + ...(namespacePublicControlState + ? { + [availableControlsPanels.NAMESPACE]: getControlPanelFromOptionsListPublicControlState( + availableControlsPanels.NAMESPACE, + namespacePublicControlState + ), + } + : {}), + }; +}; + +const getControlPanelFromOptionsListPublicControlState = ( + controlId: string, + publicControlState: OptionsListControlOption +): ControlPanels[string] => { + const defaultControlPanelConfig = controlPanelConfigs[controlId]; + + return { + ...defaultControlPanelConfig, + explicitInput: { + ...defaultControlPanelConfig.explicitInput, + exclude: publicControlState.mode === 'exclude', + ...(publicControlState.selection.type === 'exists' + ? { + existsSelected: true, + } + : { + selectedOptions: publicControlState.selection.selectedOptions, + }), + }, + }; +}; diff --git a/x-pack/plugins/log_explorer/public/controller/types.ts b/x-pack/plugins/log_explorer/public/controller/types.ts new file mode 100644 index 0000000000000..50eb259d38cb3 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/types.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryState } from '@kbn/data-plugin/public'; +import { DiscoverContainerProps } from '@kbn/discover-plugin/public'; +import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { Observable } from 'rxjs'; +import { + availableControlsPanels, + DatasetSelectionPlain, + DisplayOptions, + PartialDisplayOptions, +} from '../../common'; +import { IDatasetsClient } from '../services/datasets'; +import { + LogExplorerControllerStateMachine, + LogExplorerControllerStateService, +} from '../state_machines/log_explorer_controller'; +import { LogExplorerCustomizations } from './controller_customizations'; + +export interface LogExplorerController { + actions: {}; + customizations: LogExplorerCustomizations; + datasetsClient: IDatasetsClient; + discoverServices: LogExplorerDiscoverServices; + event$: Observable; + service: LogExplorerControllerStateService; + state$: Observable; + stateMachine: LogExplorerControllerStateMachine; +} + +export type LogExplorerDiscoverServices = Pick< + Required, + 'data' | 'filterManager' | 'timefilter' | 'uiSettings' | 'history' +> & { + urlStateStorage: IKbnUrlStateStorage; +}; + +export interface OptionsListControlOption { + mode: 'include' | 'exclude'; + selection: + | { + type: 'options'; + selectedOptions: string[]; + } + | { + type: 'exists'; + }; +} + +export interface ControlOptions { + [availableControlsPanels.NAMESPACE]?: OptionsListControlOption; +} + +// we might want to wrap this into an object that has a "state value" laster +export type LogExplorerPublicState = QueryState & + DisplayOptions & { + controls: ControlOptions; + datasetSelection: DatasetSelectionPlain; + }; + +export type LogExplorerPublicStateUpdate = QueryState & + PartialDisplayOptions & { + controls?: ControlOptions; + datasetSelection?: DatasetSelectionPlain; + }; + +// a placeholder for now +export type LogExplorerPublicEvent = never; diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx index c315971fb23a5..e7b3dc6bbd431 100644 --- a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx @@ -10,21 +10,21 @@ import { Query } from '@kbn/es-query'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { useControlPanels } from '../hooks/use_control_panels'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; const DATASET_FILTERS_CUSTOMIZATION_ID = 'datasetFiltersCustomization'; interface CustomDatasetFiltersProps { - logExplorerProfileStateService: LogExplorerProfileStateService; + logExplorerControllerStateService: LogExplorerControllerStateService; data: DataPublicPluginStart; } const CustomDatasetFilters = ({ - logExplorerProfileStateService, + logExplorerControllerStateService, data, }: CustomDatasetFiltersProps) => { const { getInitialInput, setControlGroupAPI, query, filters, timeRange } = useControlPanels( - logExplorerProfileStateService, + logExplorerControllerStateService, data ); diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx index 5b9fc3223fddf..a93a893bb20e9 100644 --- a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx @@ -15,15 +15,15 @@ import { DataViewsProvider, useDataViewsContext } from '../hooks/use_data_views' import { useEsql } from '../hooks/use_esql'; import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations'; import { IDatasetsClient } from '../services/datasets'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; interface CustomDatasetSelectorProps { - logExplorerProfileStateService: LogExplorerProfileStateService; + logExplorerControllerStateService: LogExplorerControllerStateService; } -export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => { +export const CustomDatasetSelector = withProviders(({ logExplorerControllerStateService }) => { const { datasetSelection, handleDatasetSelectionChange } = useDatasetSelection( - logExplorerProfileStateService + logExplorerControllerStateService ); const { @@ -111,13 +111,13 @@ function withProviders(Component: React.FunctionComponent - + diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx index 2fd71f948146f..b94cc930cafec 100644 --- a/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx @@ -5,46 +5,41 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { FlyoutDetail } from '../components/flyout_detail/flyout_detail'; -import { FlyoutProps } from '../components/flyout_detail'; -import { useLogExplorerCustomizationsContext } from '../hooks/use_log_explorer_customizations'; +import { LogExplorerFlyoutContentProps } from '../components/flyout_detail'; +import { useLogExplorerControllerContext } from '../controller'; -export const CustomFlyoutContent = ({ - actions, - dataView, - doc, - renderDefaultContent, -}: FlyoutProps) => { - const { flyout } = useLogExplorerCustomizationsContext(); +export const CustomFlyoutContent = (props: LogExplorerFlyoutContentProps) => { + const { + customizations: { flyout }, + } = useLogExplorerControllerContext(); - const renderPreviousContent = useCallback( - () => ( - <> - {/* Apply custom Log Explorer detail */} - - - - - ), - [actions, dataView, doc] + const renderCustomizedContent = useMemo( + () => flyout?.renderContent?.(renderContent) ?? renderContent, + [flyout] ); - const content = flyout?.renderContent - ? flyout?.renderContent(renderPreviousContent, { doc }) - : renderPreviousContent(); - return ( {/* Apply custom Log Explorer detail */} - {content} + {renderCustomizedContent(props)} {/* Restore default content */} - {renderDefaultContent()} + {props.renderDefaultContent()} ); }; +const renderContent = ({ actions, dataView, doc }: LogExplorerFlyoutContentProps) => ( + <> + {/* Apply custom Log Explorer detail */} + + + + +); + // eslint-disable-next-line import/no-default-export export default CustomFlyoutContent; diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_search_bar.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_search_bar.tsx new file mode 100644 index 0000000000000..1cc3d6bf95eec --- /dev/null +++ b/x-pack/plugins/log_explorer/public/customizations/custom_search_bar.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +export const createCustomSearchBar = ({ + navigation, + data, + unifiedSearch, +}: { + data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; +}) => { + const { + ui: { createTopNavWithCustomContext }, + } = navigation; + + const { + ui: { getCustomSearchBar }, + } = unifiedSearch; + + const CustomSearchBar = getCustomSearchBar(data); + + const customUnifiedSearch = { + ...unifiedSearch, + ui: { + ...unifiedSearch.ui, + SearchBar: CustomSearchBar, + AggregateQuerySearchBar: CustomSearchBar, + }, + }; + + return createTopNavWithCustomContext(customUnifiedSearch); +}; diff --git a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx index 89c585d8d201e..5712a72ed0e1c 100644 --- a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx @@ -4,19 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; + import type { CoreStart } from '@kbn/core/public'; -import { CustomizationCallback, DiscoverStateContainer } from '@kbn/discover-plugin/public'; +import type { CustomizationCallback } from '@kbn/discover-plugin/public'; import { i18n } from '@kbn/i18n'; +import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { combineLatest, from, map, Subscription, type BehaviorSubject } from 'rxjs'; -import { LogExplorerStateContainer } from '../components/log_explorer'; -import { LogExplorerCustomizations } from '../components/log_explorer/types'; -import { LogExplorerCustomizationsProvider } from '../hooks/use_log_explorer_customizations'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; -import { LogExplorerStartDeps } from '../types'; +import { waitFor } from 'xstate/lib/waitFor'; +import type { LogExplorerController } from '../controller'; +import { LogExplorerControllerProvider } from '../controller/provider'; +import type { LogExplorerStartDeps } from '../types'; import { dynamic } from '../utils/dynamic'; import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; +import { createCustomSearchBar } from './custom_search_bar'; const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters')); const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector')); @@ -24,56 +24,31 @@ const LazyCustomFlyoutContent = dynamic(() => import('./custom_flyout_content')) export interface CreateLogExplorerProfileCustomizationsDeps { core: CoreStart; - customizations: LogExplorerCustomizations; plugins: LogExplorerStartDeps; - state$?: BehaviorSubject; + controller: LogExplorerController; } export const createLogExplorerProfileCustomizations = ({ core, - customizations: logExplorerCustomizations, plugins, - state$, + controller, }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback => async ({ customizations, stateContainer }) => { - const { data, dataViews, discover } = plugins; - // Lazy load dependencies - const datasetServiceModuleLoadable = import('../services/datasets'); - const logExplorerMachineModuleLoadable = import('../state_machines/log_explorer_profile'); - - const [{ DatasetsService }, { initializeLogExplorerProfileStateService, waitForState }] = - await Promise.all([datasetServiceModuleLoadable, logExplorerMachineModuleLoadable]); - - const datasetsClient = new DatasetsService().start({ - http: core.http, - }).client; + const { discoverServices, service } = controller; + const pluginsWithOverrides = { + ...plugins, + ...discoverServices, + }; + const { data, dataViews, discover, navigation, unifiedSearch } = pluginsWithOverrides; - const logExplorerProfileStateService = initializeLogExplorerProfileStateService({ - datasetsClient, - stateContainer, - toasts: core.notifications.toasts, - }); + service.send('RECEIVED_STATE_CONTAINER', { discoverStateContainer: stateContainer }); /** * Wait for the machine to be fully initialized to set the restored selection * create the DataView and set it in the stateContainer from Discover */ - await waitForState(logExplorerProfileStateService, 'initialized'); - - /** - * Subscribe the state$ BehaviorSubject when the consumer app wants to react to state changes. - * It emits a combined state of: - * - log explorer state machine context - * - appState from the discover stateContainer - */ - let stateSubscription: Subscription; - if (state$) { - stateSubscription = createStateUpdater({ - logExplorerProfileStateService, - stateContainer, - }).subscribe(state$); - } + await waitFor(service, (state) => state.matches('initialized'), { timeout: 30000 }); /** * Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams @@ -87,20 +62,22 @@ export const createLogExplorerProfileCustomizations = return ( ); }, PrependFilterBar: () => ( - + ), + CustomSearchBar: createCustomSearchBar({ + data, + navigation, + unifiedSearch, + }), }); /** @@ -143,32 +120,13 @@ export const createLogExplorerProfileCustomizations = return ( - + - + ); }, }); - return () => { - if (stateSubscription) { - stateSubscription.unsubscribe(); - } - }; + return () => {}; }; - -const createStateUpdater = ({ - logExplorerProfileStateService, - stateContainer, -}: { - logExplorerProfileStateService: LogExplorerProfileStateService; - stateContainer: DiscoverStateContainer; -}) => { - return combineLatest([from(logExplorerProfileStateService), stateContainer.appState.state$]).pipe( - map(([logExplorerState, appState]) => ({ - logExplorerState: logExplorerState.context, - appState, - })) - ); -}; diff --git a/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx b/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx index c10cddabbe0b1..f38b902aaa045 100644 --- a/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx +++ b/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx @@ -13,16 +13,16 @@ import { Query, TimeRange } from '@kbn/es-query'; import { useQuerySubscriber } from '@kbn/unified-field-list'; import { useSelector } from '@xstate/react'; import { useCallback } from 'react'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; export const useControlPanels = ( - logExplorerProfileStateService: LogExplorerProfileStateService, + logExplorerControllerStateService: LogExplorerControllerStateService, data: DataPublicPluginStart ) => { const { query, filters, fromDate, toDate } = useQuerySubscriber({ data }); const timeRange: TimeRange = { from: fromDate!, to: toDate! }; - const controlPanels = useSelector(logExplorerProfileStateService, (state) => { + const controlPanels = useSelector(logExplorerControllerStateService, (state) => { if (!('controlPanels' in state.context)) return; return state.context.controlPanels; }); @@ -45,12 +45,12 @@ export const useControlPanels = ( const setControlGroupAPI = useCallback( (controlGroupAPI: ControlGroupAPI) => { - logExplorerProfileStateService.send({ + logExplorerControllerStateService.send({ type: 'INITIALIZE_CONTROL_GROUP_API', controlGroupAPI, }); }, - [logExplorerProfileStateService] + [logExplorerControllerStateService] ); return { getInitialInput, setControlGroupAPI, query, filters, timeRange }; diff --git a/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts b/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts index 6bff4055f3635..1f611da05c8cc 100644 --- a/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts @@ -8,20 +8,20 @@ import { useSelector } from '@xstate/react'; import { useCallback } from 'react'; import { DatasetSelectionChange } from '../../common/dataset_selection'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; export const useDatasetSelection = ( - logExplorerProfileStateService: LogExplorerProfileStateService + logExplorerControllerStateService: LogExplorerControllerStateService ) => { - const datasetSelection = useSelector(logExplorerProfileStateService, (state) => { + const datasetSelection = useSelector(logExplorerControllerStateService, (state) => { return state.context.datasetSelection; }); const handleDatasetSelectionChange: DatasetSelectionChange = useCallback( (data) => { - logExplorerProfileStateService.send({ type: 'UPDATE_DATASET_SELECTION', data }); + logExplorerControllerStateService.send({ type: 'UPDATE_DATASET_SELECTION', data }); }, - [logExplorerProfileStateService] + [logExplorerControllerStateService] ); return { datasetSelection, handleDatasetSelectionChange }; diff --git a/x-pack/plugins/log_explorer/public/hooks/use_log_explorer_customizations.ts b/x-pack/plugins/log_explorer/public/hooks/use_log_explorer_customizations.ts deleted file mode 100644 index 0557e17761cb4..0000000000000 --- a/x-pack/plugins/log_explorer/public/hooks/use_log_explorer_customizations.ts +++ /dev/null @@ -1,17 +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 createContainer from 'constate'; -import { LogExplorerCustomizations } from '../components/log_explorer/types'; - -interface UseLogExplorerCustomizationsDeps { - value: LogExplorerCustomizations; -} - -const useLogExplorerCustomizations = ({ value }: UseLogExplorerCustomizationsDeps) => value; - -export const [LogExplorerCustomizationsProvider, useLogExplorerCustomizationsContext] = - createContainer(useLogExplorerCustomizations); diff --git a/x-pack/plugins/log_explorer/public/index.ts b/x-pack/plugins/log_explorer/public/index.ts index 1ca7f37aa4c9b..005b5cca07a14 100644 --- a/x-pack/plugins/log_explorer/public/index.ts +++ b/x-pack/plugins/log_explorer/public/index.ts @@ -8,12 +8,20 @@ import type { PluginInitializerContext } from '@kbn/core/public'; import type { LogExplorerConfig } from '../common/plugin_config'; import { LogExplorerPlugin } from './plugin'; -export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types'; -export type { LogExplorerStateContainer } from './components/log_explorer'; export type { + CreateLogExplorerController, + LogExplorerController, LogExplorerCustomizations, LogExplorerFlyoutContentProps, -} from './components/log_explorer/types'; + LogExplorerPublicState, + LogExplorerPublicStateUpdate, +} from './controller'; +export type { LogExplorerControllerContext } from './state_machines/log_explorer_controller'; +export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types'; +export { + getDiscoverColumnsFromDisplayOptions, + getDiscoverGridFromDisplayOptions, +} from './utils/convert_discover_app_state'; export function plugin(context: PluginInitializerContext) { return new LogExplorerPlugin(context); diff --git a/x-pack/plugins/log_explorer/public/plugin.ts b/x-pack/plugins/log_explorer/public/plugin.ts index 3c637b6b06caf..8c527c28fb8b2 100644 --- a/x-pack/plugins/log_explorer/public/plugin.ts +++ b/x-pack/plugins/log_explorer/public/plugin.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { LogExplorerLocatorDefinition, LogExplorerLocators } from '../common/locators'; import { createLogExplorer } from './components/log_explorer'; -import { +import { createLogExplorerControllerLazyFactory } from './controller/lazy_create_controller'; +import type { LogExplorerPluginSetup, LogExplorerPluginStart, LogExplorerSetupDeps, @@ -48,8 +49,14 @@ export class LogExplorerPlugin implements Plugin => + async (context) => { + if (!('discoverStateContainer' in context)) return; + return context.controlPanels + ? constructControlPanelsWithDataViewId(context.discoverStateContainer, context.controlPanels) + : undefined; + }; + +export const subscribeControlGroup = + (): InvokeCreator => + (context) => + (send) => { + if (!('controlGroupAPI' in context)) return; + if (!('discoverStateContainer' in context)) return; + const { discoverStateContainer } = context; + + const filtersSubscription = context.controlGroupAPI.onFiltersPublished$.subscribe( + (newFilters) => { + discoverStateContainer.internalState.transitions.setCustomFilters(newFilters); + discoverStateContainer.actions.fetchData(); + } + ); + + const inputSubscription = context.controlGroupAPI.getInput$().subscribe(({ panels }) => { + if (!deepEqual(panels, context.controlPanels)) { + send({ type: 'UPDATE_CONTROL_PANELS', controlPanels: panels }); + } + }); + + return () => { + filtersSubscription.unsubscribe(); + inputSubscription.unsubscribe(); + }; + }; + +export const updateControlPanels = + (): InvokeCreator => + async (context, event) => { + if (!('controlGroupAPI' in context)) return; + if (!('discoverStateContainer' in context)) return; + const { discoverStateContainer } = context; + + const newControlPanels = + ('controlPanels' in event && event.controlPanels) || context.controlPanels; + + if (!newControlPanels) return undefined; + + const controlPanelsWithId = constructControlPanelsWithDataViewId( + discoverStateContainer, + newControlPanels! + ); + + context.controlGroupAPI.updateInput({ panels: controlPanelsWithId }); + + return controlPanelsWithId; + }; + +const constructControlPanelsWithDataViewId = ( + stateContainer: DiscoverStateContainer, + newControlPanels: ControlPanels +) => { + const dataView = stateContainer.internalState.getState().dataView!; + + const validatedControlPanels = isValidState(newControlPanels) + ? newControlPanels + : getVisibleControlPanelsConfig(dataView); + + const controlsPanelsWithId = mergeDefaultPanelsWithControlPanels( + dataView, + validatedControlPanels! + ); + + return controlsPanelsWithId; +}; + +const isValidState = (state: ControlPanels | undefined | null): boolean => { + return Object.keys(state ?? {}).length > 0 && ControlPanelRT.is(state); +}; + +const getVisibleControlPanels = (dataView: DataView | undefined) => + availableControlPanelFields.filter( + (panelKey) => dataView?.fields.getByName(panelKey) !== undefined + ); + +export const getVisibleControlPanelsConfig = (dataView?: DataView) => { + return getVisibleControlPanels(dataView).reduce((panelsMap, panelKey) => { + const config = controlPanelConfigs[panelKey]; + + return { ...panelsMap, [panelKey]: config }; + }, {} as ControlPanels); +}; + +const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId: string = '') => { + return mapValues(controlPanels, (controlPanelConfig) => ({ + ...controlPanelConfig, + explicitInput: { ...controlPanelConfig.explicitInput, dataViewId }, + })); +}; + +const mergeDefaultPanelsWithControlPanels = (dataView: DataView, urlPanels: ControlPanels) => { + // Get default panel configs from existing fields in data view + const visiblePanels = getVisibleControlPanelsConfig(dataView); + + // Get list of panel which can be overridden to avoid merging additional config from url + const existingKeys = Object.keys(visiblePanels); + const controlPanelsToOverride = pick(urlPanels, existingKeys); + + // Merge default and existing configs and add dataView.id to each of them + return addDataViewIdToControlPanels( + { ...visiblePanels, ...controlPanelsToOverride }, + dataView.id + ); +}; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/data_view_service.ts similarity index 64% rename from x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/data_view_service.ts index fa2b2992ee76b..76ac9c0c82f23 100644 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/data_view_service.ts @@ -5,23 +5,15 @@ * 2.0. */ -import { DiscoverStateContainer } from '@kbn/discover-plugin/public'; import { InvokeCreator } from 'xstate'; -import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types'; - -interface LogExplorerProfileDataViewStateDependencies { - stateContainer: DiscoverStateContainer; -} +import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; export const createAndSetDataView = - ({ - stateContainer, - }: LogExplorerProfileDataViewStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => + (): InvokeCreator => async (context) => { - const dataView = await stateContainer.actions.createAndAppendAdHocDataView( + if (!('discoverStateContainer' in context)) return; + const { discoverStateContainer } = context; + const dataView = await discoverStateContainer.actions.createAndAppendAdHocDataView( context.datasetSelection.toDataviewSpec() ); /** @@ -32,5 +24,5 @@ export const createAndSetDataView = * to the existing one or the default logs-*. * We set explicitly the data view here to be used when restoring the data view on the initial load. */ - stateContainer.actions.setDataView(dataView); + discoverStateContainer.actions.setDataView(dataView); }; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/discover_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/discover_service.ts new file mode 100644 index 0000000000000..512523c24c8f7 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/discover_service.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; +import { ActionFunction, actions, InvokeCallback } from 'xstate'; +import { + getChartDisplayOptionsFromDiscoverAppState, + getDiscoverAppStateFromContext, + getGridColumnDisplayOptionsFromDiscoverAppState, + getGridRowsDisplayOptionsFromDiscoverAppState, + getQueryStateFromDiscoverAppState, +} from '../../../../utils/convert_discover_app_state'; +import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; + +export const subscribeToDiscoverState = + () => + ( + context: LogExplorerControllerContext + ): InvokeCallback => + (send, onEvent) => { + if (!('discoverStateContainer' in context)) { + throw new Error('Failed to subscribe to the Discover state: no state container in context.'); + } + + const { appState } = context.discoverStateContainer; + + const subscription = appState.state$.subscribe({ + next: (newAppState) => { + if (isEmpty(newAppState)) { + return; + } + + send({ + type: 'RECEIVE_DISCOVER_APP_STATE', + appState: newAppState, + }); + }, + }); + + return () => { + subscription.unsubscribe(); + }; + }; + +export const updateContextFromDiscoverAppState = actions.assign< + LogExplorerControllerContext, + LogExplorerControllerEvent +>((context, event) => { + if ('appState' in event && event.type === 'RECEIVE_DISCOVER_APP_STATE') { + return { + chart: { + ...context.chart, + ...getChartDisplayOptionsFromDiscoverAppState(event.appState), + }, + grid: { + columns: + getGridColumnDisplayOptionsFromDiscoverAppState(event.appState) ?? context.grid.columns, + rows: { + ...context.grid.rows, + ...getGridRowsDisplayOptionsFromDiscoverAppState(event.appState), + }, + }, + ...getQueryStateFromDiscoverAppState(event.appState), + }; + } + + return {}; +}); + +export const updateDiscoverAppStateFromContext: ActionFunction< + LogExplorerControllerContext, + LogExplorerControllerEvent +> = (context, _event) => { + if (!('discoverStateContainer' in context)) { + return; + } + + context.discoverStateContainer.appState.update(getDiscoverAppStateFromContext(context)); +}; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/selection_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/selection_service.ts similarity index 79% rename from x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/selection_service.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/selection_service.ts index 6de3d24025802..a5aeac069fc32 100644 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/selection_service.ts +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/selection_service.ts @@ -6,21 +6,21 @@ */ import { InvokeCreator } from 'xstate'; -import { Dataset } from '../../../../common/datasets'; -import { SingleDatasetSelection } from '../../../../common/dataset_selection'; -import { IDatasetsClient } from '../../../services/datasets'; -import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types'; +import { Dataset } from '../../../../../common/datasets'; +import { SingleDatasetSelection } from '../../../../../common/dataset_selection'; +import { IDatasetsClient } from '../../../../services/datasets'; +import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; -interface LogExplorerProfileUrlStateDependencies { +interface LogExplorerControllerUrlStateDependencies { datasetsClient: IDatasetsClient; } export const validateSelection = ({ datasetsClient, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent + }: LogExplorerControllerUrlStateDependencies): InvokeCreator< + LogExplorerControllerContext, + LogExplorerControllerEvent > => (context) => async (send) => { diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/timefilter_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/timefilter_service.ts new file mode 100644 index 0000000000000..124a6778493b0 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/timefilter_service.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryStart } from '@kbn/data-plugin/public'; +import { map, merge, Observable } from 'rxjs'; +import { ActionFunction, actions } from 'xstate'; +import type { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; + +export const subscribeToTimefilterService = + (query: QueryStart) => (): Observable => { + const { + timefilter: { timefilter }, + } = query; + + const time$ = timefilter.getTimeUpdate$().pipe( + map( + (): LogExplorerControllerEvent => ({ + type: 'RECEIVE_TIMEFILTER_TIME', + time: timefilter.getTime(), + }) + ) + ); + + const refreshInterval$ = timefilter.getRefreshIntervalUpdate$().pipe( + map( + (): LogExplorerControllerEvent => ({ + type: 'RECEIVE_TIMEFILTER_REFRESH_INTERVAL', + refreshInterval: timefilter.getRefreshInterval(), + }) + ) + ); + + return merge(time$, refreshInterval$); + }; + +export const updateContextFromTimefilter = actions.assign< + LogExplorerControllerContext, + LogExplorerControllerEvent +>((context, event) => { + if (event.type === 'RECEIVE_TIMEFILTER_TIME' && 'time' in event) { + return { + time: event.time, + }; + } + + if (event.type === 'RECEIVE_TIMEFILTER_REFRESH_INTERVAL' && 'refreshInterval' in event) { + return { + refreshInterval: event.refreshInterval, + }; + } + + return {}; +}); + +export const updateTimefilterFromContext = + (query: QueryStart): ActionFunction => + (context, _event) => { + if (context.time != null) { + query.timefilter.timefilter.setTime(context.time); + } + if (context.refreshInterval != null) { + query.timefilter.timefilter.setRefreshInterval(context.refreshInterval); + } + }; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/state_machine.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/state_machine.ts new file mode 100644 index 0000000000000..ef9c87ee7ae14 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/state_machine.ts @@ -0,0 +1,298 @@ +/* + * 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 { IToasts } from '@kbn/core/public'; +import { QueryStart } from '@kbn/data-plugin/public'; +import { actions, createMachine, interpret, InterpreterFrom, raise } from 'xstate'; +import { ControlPanelRT } from '../../../../common/control_panels'; +import { isDatasetSelection } from '../../../../common/dataset_selection'; +import { IDatasetsClient } from '../../../services/datasets'; +import { DEFAULT_CONTEXT } from './defaults'; +import { + createCreateDataViewFailedNotifier, + createDatasetSelectionRestoreFailedNotifier, +} from './notifications'; +import { + initializeControlPanels, + subscribeControlGroup, + updateControlPanels, +} from './services/control_panels'; +import { createAndSetDataView } from './services/data_view_service'; +import { + subscribeToDiscoverState, + updateContextFromDiscoverAppState, + updateDiscoverAppStateFromContext, +} from './services/discover_service'; +import { validateSelection } from './services/selection_service'; +import { + subscribeToTimefilterService, + updateContextFromTimefilter, + updateTimefilterFromContext, +} from './services/timefilter_service'; +import { + LogExplorerControllerContext, + LogExplorerControllerEvent, + LogExplorerControllerTypeState, +} from './types'; + +export const createPureLogExplorerControllerStateMachine = ( + initialContext: LogExplorerControllerContext +) => + /** @xstate-layout N4IgpgJg5mDOIC5QBkD2UCiAPADgG1QCcxCBhVAOwBdDU88SA6AVwoEt2q2BDPNgL0gBiAEoZSGAJIA1DABEA+gGUAKgEEVGBaQDyAOXWS9GEQG0ADAF1EoHKlhsulGyCyIAtACYAbIwDsAIzmAJwAzH4ArAA0IACeiJ5+AByMwZ6hnokALD6e5kneAL6FMWiYuATEZJQ0dAyEjByOPHz8HFBy3FTc0mxgAO5CEJRgjRQAbqgA1qNl2PhEJOTUtPRMTVy8Au2d3b0DCByTAMZdbJQWlpcudg5OFC5uCAGejObentFxiBHBwYyeLIBX5JfLeX7BCLFUroeaVJY1Vb1MbNLZtCgdLo9PqDEi0Br4LoAMyIAFtGHMKotqis6utOC1thjdtiDkdUKd7pdrkgQLdms5eU9sowAqEkn8IuLIUDvN4YvEEFksqE3hFQaEglkperPNCQJSFlVlrU1g0Noz0VATasAArcChgPCwIYjMaTGYU2FU42Iunmhlo9o2uj2x3Ow4TDlnC5WHm2ewCh5Cjy+YLeJJ+cGfTMS96fBWIJJJAKMULa8wZbzmPyfbxZfWG+E003Ii1BjEhvBhp0uvFERiEqgkwjkpvUrttwOtYN+7sO3uRk4xijcqw3RP3R4JPyqgJZDM14sygJywsIXf-TV+LLBEIFXc5Rveo0I2lmlGbVrCMQSGRaORJCUXRZBEBQ1FtW1lHUTR4z5TdzmTUBhVCdNRX3QEQmSbUknPCJAn8IJawiTxgm1dIoRKA0X2bSd6VRb8IFEcQpFkBQAEUAFUTAATWgjQMDg-ktxTBBMIiRhiyzcUpSzSJz0fRhvACPws0rEsIgietn3KV8WyReivwESBGAgLFYDAKglCdMBjnuRhxi2MyuAxayGDsxChGQIDND0BQVB0bQAAk1D0ABxDAlCEhDBWQxAyN8W8wkCJIfDSIFzzlLJ0KyfIwmVAIklCUIdLhCc5ynBjjIgUzzMstzbPsxy+Gc9oGo8yghE4205AEhRevUJQMBUZQMGQcQVEkfRoruRDtwQB9RXBVCIn3cwQnBc8S1VcEVQ0yIPgCAJSp9N9W0My0TOc7gLKsmyOooBynLOVz7vuIQBrUIaRqG8bSEm-QFDEVQdDEBQADE1EkZBOLEGak3m8FS1COUAmCNS5Wvc80hSYtPFS4rvDvJJNJOvS6IDKrBBq67bva+y2AgBgup6vrPu+0a-oBvR4ZEuLnh8FJCaK6t8drcstrFJSIj24EDs8I6ydoiqLrRK66ru9yGaZsAPo0L7hs5iapr84GArByHodhwT115YS5tE4FwUYFUifMdJlQid2sklnaZfFOWtIV46qPHX130qozqdq7o6bexCWBwVrmSxfZBmGR13WmWYaPKiPVcYmObvq+PKET5PMT2HEl2jLk41thNZti1xEEK4qlKBPIUfVHJQmxtCIVS9MvewopQ9z8PzspqP1djkutYT5gk5eyvWVxQh8UHPBiTJL1dOV-Pp8ummNfpxfl5c1e05rzlELXaw7ZipCW+edMUi9kIwh77xNXPIJITeGkfKy0ayniVnnKen5j6MGOHOMKtAl6wBYOwac1UhBGEkJNNQ3kABaWhdAGBEDoZACgwpEO6uBW0kheYO35nmAEgI0afGVAHXC3xnjlgkrlSUwQmHuzHjCfeECDJHzVjVWB754GoEQY0HWet1AKGkJIDAAB1BQ3UBryBoc3J4LwDwAk1Ojcih5wh-ylNlIqljEj4wopRQRZVJ4iKgWImBcCEE4CQYzZmGi+oEJUEQkhtpQpjSig3eCTdn66JRn4VImRqx+DCLwzIAQzFkRdvuDMqk-D5FvHY6iQjHH+mcYXCRpopEyKXhXLsPZnSukzuyT0YczpOPbCUtx0iPHlxXtUhcEZ2S31jFcMJ9sdGt3RuYN4GRAShG-r-dhKkMzSzvFpXcRNEgh3sadfSRTWnVVcZI9xSDKndLnDUvsG8BxDhHGOCezSdmoOjqU1Y5TOnHMvj08MsAb4rnvhuCJ80VJpKSNqLIoJ+FpBvH-QIEl1rlhJumImaRKJUQoKgCAcAXBNO2WaP5CNRLuBvIREI4QviKgSf4CUwQSYkQPOkMi4DCkflYLs6muK+YvxyH-bwMT8Z-D5fyv4Aj8kOLuR+FlOxU44jZbQl+7hSye01AWdhyoJnYV+DM7UMzUoMtFZHS0s53xnOlaMhAXgJlZCzEHJK-KCgKX3GWFSt57zcvLHqceBTdUF2qsayJPwSKpA+JWBWvwgTanPKhV4CteEkTlCEHIwQdXYr1S42m89GoypGb6hA1ZVQo2UujasmMVJ4QIoENIGZgS5MSImimxS9mps1umsuzVGYrzPs3TN81Ah7hybC3aeQ-CZV+ACJhqUf5BoKDWlWojC4NvbY9LxYAfUAu1DyksCSSb1nMAeNhioso5TyuWIERUSrupFUmr10c52l0em8iVVcBjLsdqeGJwJULig3WRIq-dspVgjcCI8B4p2HzrY89piCn382rBM9MXdg1kX3KS1unxX0oyBO+9U6M8lYtrSykyTy6AvKOSgqmkBIMv3rCkWDQbgQIbDfMlGKQxTZlPGkfhwHIF4fEeBzpi7yO6KlP8UiylbzZLyPjP+R6lLdwVms9jZ6tm4YefhnjRyL4GtNEax+-zHZYQBH8EInxNLrsHfMt2bxKygt+HKP4nxijFCAA */ + createMachine< + LogExplorerControllerContext, + LogExplorerControllerEvent, + LogExplorerControllerTypeState + >( + { + context: initialContext, + predictableActionArguments: true, + id: 'LogExplorerController', + initial: 'uninitialized', + states: { + uninitialized: { + on: { + RECEIVED_STATE_CONTAINER: { + target: 'initializingDataView', + actions: ['storeDiscoverStateContainer'], + }, + }, + }, + initializingDataView: { + invoke: { + src: 'createDataView', + onDone: { + target: 'initializingControlPanels', + actions: ['updateDiscoverAppStateFromContext', 'updateTimefilterFromContext'], + }, + onError: { + target: 'initialized', + actions: [ + 'notifyCreateDataViewFailed', + 'updateDiscoverAppStateFromContext', + 'updateTimefilterFromContext', + ], + }, + }, + }, + initializingControlPanels: { + invoke: { + src: 'initializeControlPanels', + onDone: { + target: 'initialized', + actions: ['storeControlPanels'], + }, + onError: { + target: 'initialized', + }, + }, + }, + initialized: { + type: 'parallel', + invoke: [ + { + src: 'discoverStateService', + id: 'discoverStateService', + }, + { + src: 'timefilterService', + id: 'timefilterService', + }, + ], + states: { + datasetSelection: { + initial: 'validatingSelection', + states: { + validatingSelection: { + invoke: { + src: 'validateSelection', + }, + on: { + LISTEN_TO_CHANGES: { + target: 'idle', + }, + UPDATE_DATASET_SELECTION: { + target: 'updatingDataView', + actions: ['storeDatasetSelection'], + }, + DATASET_SELECTION_RESTORE_FAILURE: { + target: 'updatingDataView', + actions: ['notifyDatasetSelectionRestoreFailed'], + }, + }, + }, + idle: { + on: { + UPDATE_DATASET_SELECTION: { + target: 'updatingDataView', + actions: ['storeDatasetSelection'], + }, + DATASET_SELECTION_RESTORE_FAILURE: { + target: 'updatingDataView', + actions: ['notifyDatasetSelectionRestoreFailed'], + }, + }, + }, + updatingDataView: { + invoke: { + src: 'createDataView', + onDone: { + target: 'idle', + actions: ['notifyDataViewUpdate'], + }, + onError: { + target: 'idle', + actions: ['notifyCreateDataViewFailed'], + }, + }, + }, + }, + }, + controlGroups: { + initial: 'uninitialized', + states: { + uninitialized: { + on: { + INITIALIZE_CONTROL_GROUP_API: { + target: 'idle', + cond: 'controlGroupAPIExists', + actions: ['storeControlGroupAPI'], + }, + }, + }, + idle: { + invoke: { + src: 'subscribeControlGroup', + }, + on: { + DATA_VIEW_UPDATED: { + target: 'updatingControlPanels', + }, + UPDATE_CONTROL_PANELS: { + target: 'updatingControlPanels', + }, + }, + }, + updatingControlPanels: { + invoke: { + src: 'updateControlPanels', + onDone: { + target: 'idle', + actions: ['storeControlPanels'], + }, + onError: { + target: 'idle', + }, + }, + }, + }, + }, + }, + on: { + RECEIVE_DISCOVER_APP_STATE: { + actions: ['updateContextFromDiscoverAppState'], + }, + RECEIVE_QUERY_STATE: { + actions: ['updateQueryStateFromQueryServiceState'], + }, + RECEIVE_TIMEFILTER_TIME: { + actions: ['updateContextFromTimefilter'], + }, + RECEIVE_TIMEFILTER_REFRESH_INTERVAL: { + actions: ['updateContextFromTimefilter'], + }, + }, + }, + }, + }, + { + actions: { + storeDatasetSelection: actions.assign((_context, event) => + 'data' in event && isDatasetSelection(event.data) + ? { + datasetSelection: event.data, + } + : {} + ), + storeDiscoverStateContainer: actions.assign((_context, event) => + 'discoverStateContainer' in event + ? { + discoverStateContainer: event.discoverStateContainer, + } + : {} + ), + storeControlGroupAPI: actions.assign((_context, event) => + 'controlGroupAPI' in event + ? { + controlGroupAPI: event.controlGroupAPI, + } + : {} + ), + storeControlPanels: actions.assign((_context, event) => + 'data' in event && ControlPanelRT.is(event.data) + ? { + controlPanels: event.data, + } + : {} + ), + notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'), + updateContextFromDiscoverAppState, + updateDiscoverAppStateFromContext, + updateContextFromTimefilter, + }, + guards: { + controlGroupAPIExists: (_context, event) => { + return 'controlGroupAPI' in event && event.controlGroupAPI != null; + }, + }, + } + ); + +export interface LogExplorerControllerStateMachineDependencies { + datasetsClient: IDatasetsClient; + initialContext?: LogExplorerControllerContext; + query: QueryStart; + toasts: IToasts; +} + +export const createLogExplorerControllerStateMachine = ({ + datasetsClient, + initialContext = DEFAULT_CONTEXT, + query, + toasts, +}: LogExplorerControllerStateMachineDependencies) => + createPureLogExplorerControllerStateMachine(initialContext).withConfig({ + actions: { + notifyCreateDataViewFailed: createCreateDataViewFailedNotifier(toasts), + notifyDatasetSelectionRestoreFailed: createDatasetSelectionRestoreFailedNotifier(toasts), + updateTimefilterFromContext: updateTimefilterFromContext(query), + }, + services: { + createDataView: createAndSetDataView(), + initializeControlPanels: initializeControlPanels(), + subscribeControlGroup: subscribeControlGroup(), + updateControlPanels: updateControlPanels(), + validateSelection: validateSelection({ datasetsClient }), + discoverStateService: subscribeToDiscoverState(), + timefilterService: subscribeToTimefilterService(query), + }, + }); + +export const initializeLogExplorerControllerStateService = ( + deps: LogExplorerControllerStateMachineDependencies +) => { + const machine = createLogExplorerControllerStateMachine(deps); + return interpret(machine).start(); +}; + +export type LogExplorerControllerStateService = InterpreterFrom< + typeof createLogExplorerControllerStateMachine +>; + +export type LogExplorerControllerStateMachine = ReturnType< + typeof createLogExplorerControllerStateMachine +>; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/types.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/types.ts new file mode 100644 index 0000000000000..73d87913a8150 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/types.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ControlGroupAPI } from '@kbn/controls-plugin/public'; +import { QueryState, RefreshInterval, TimeRange } from '@kbn/data-plugin/common'; +import { DiscoverAppState, DiscoverStateContainer } from '@kbn/discover-plugin/public'; +import { DoneInvokeEvent } from 'xstate'; +import { ControlPanels, DisplayOptions } from '../../../../common'; +import type { DatasetEncodingError, DatasetSelection } from '../../../../common/dataset_selection'; + +export interface WithDatasetSelection { + datasetSelection: DatasetSelection; +} + +export interface WithControlPanelGroupAPI { + controlGroupAPI: ControlGroupAPI; +} + +export interface WithControlPanels { + controlPanels?: ControlPanels; +} + +export type WithQueryState = QueryState; + +export type WithDisplayOptions = DisplayOptions; + +export interface WithDiscoverStateContainer { + discoverStateContainer: DiscoverStateContainer; +} + +export type DefaultLogExplorerControllerState = WithDatasetSelection & + WithQueryState & + WithDisplayOptions; + +export type LogExplorerControllerTypeState = + | { + value: 'uninitialized'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initializingDataView'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initializingControlPanels'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initializingStateContainer'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initialized'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.validatingSelection'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.idle'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.updatingDataView'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.updatingStateContainer'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.controlGroups.uninitialized'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.controlGroups.idle'; + context: WithDatasetSelection & + WithControlPanelGroupAPI & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.controlGroups.updatingControlPanels'; + context: WithDatasetSelection & + WithControlPanelGroupAPI & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + }; + +export type LogExplorerControllerContext = LogExplorerControllerTypeState['context']; + +export type LogExplorerControllerStateValue = LogExplorerControllerTypeState['value']; + +export type LogExplorerControllerEvent = + | { + type: 'RECEIVED_STATE_CONTAINER'; + discoverStateContainer: DiscoverStateContainer; + } + | { + type: 'LISTEN_TO_CHANGES'; + } + | { + type: 'UPDATE_DATASET_SELECTION'; + data: DatasetSelection; + } + | { + type: 'DATASET_SELECTION_RESTORE_FAILURE'; + } + | { + type: 'INITIALIZE_CONTROL_GROUP_API'; + controlGroupAPI: ControlGroupAPI | undefined; + } + | { + type: 'UPDATE_CONTROL_PANELS'; + controlPanels: ControlPanels | null; + } + | { + type: 'RECEIVE_DISCOVER_APP_STATE'; + appState: DiscoverAppState; + } + | { + type: 'RECEIVE_TIMEFILTER_TIME'; + time: TimeRange; + } + | { + type: 'RECEIVE_TIMEFILTER_REFRESH_INTERVAL'; + refreshInterval: RefreshInterval; + } + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts deleted file mode 100644 index 4fa1673b5879d..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts +++ /dev/null @@ -1,279 +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 { IToasts } from '@kbn/core/public'; -import { DiscoverStateContainer } from '@kbn/discover-plugin/public'; -import { actions, createMachine, interpret, InterpreterFrom, raise } from 'xstate'; -import { IDatasetsClient } from '../../../services/datasets'; -import { isDatasetSelection } from '../../../../common/dataset_selection'; -import { createAndSetDataView } from './data_view_service'; -import { validateSelection } from './selection_service'; -import { DEFAULT_CONTEXT } from './defaults'; -import { - createCreateDataViewFailedNotifier, - createDatasetSelectionRestoreFailedNotifier, -} from './notifications'; -import { - ControlPanelRT, - LogExplorerProfileContext, - LogExplorerProfileEvent, - LogExplorerProfileTypeState, -} from './types'; -import { - initializeControlPanels, - initializeFromUrl, - listenUrlChange, - subscribeControlGroup, - updateControlPanels, - updateStateContainer, -} from './url_state_storage_service'; - -export const createPureLogExplorerProfileStateMachine = ( - initialContext: LogExplorerProfileContext -) => - /** @xstate-layout N4IgpgJg5mDOIC5QBkD2UCiAPADgG1QCcxCAFQ1AMwEs8wA6AVwDtrWAXagQz2oC9IAYgDaABgC6iUDlSxqnVMykgsiAIwAOAMz0NagEwBWUdoBsWgOxaAnEYA0IAJ7rt9UxesWD+2xf2WAXwCHNExcAmIyCho6ejZ5bl4+NigAMQoAWwBVQjxBCEUGNgA3VABrBlDsfCIScipaIo5E-hT01GzchBLUAGMuBWYxcWHlGTlB5VUEYw1dfQ1RQ30LC0NrbX0HZwQ1CwAWemtDLTV991FTPWWtIJD0aoi66Ma45p5W5jTMnLySCkI9HwA0oRAy9Cq4VqUQasXinA+yS+7U6eG6zFK-UGw1GSBA4wSiimiFm80Wy1W60220QenoxlEjP2J32alEmmsdxAkJqkXqMSaCURKQAIgMuAA1ahgADu+UKb1KFQhDyhfJecPeSVF4qlsvRmIG1EUOIkY1khKUeOmPkM9D8pkMFm0bMpFhpM3Obi0tqdxk0Fi5PKeMIFbyF2q+YvYkulcv+RCBeBBYJVYV5z1hgoRkag0dj+p6WONQwkuOkFsm1sQtvt+kdztOojdHquonoah91h9WkWS1MQdVGdDr3hLSRUAAwop2BQ8KQuMwwHhYPKl4rypUhyH+aOtZ8pzO5wulyuDX0jSay2a8QSq6BphZTNZ6Po1O+jMy++YPScLEdLi0UwDG7Z9jkHdMdw1bNxxSadmFnVB50XZdVwTQFgXYUFCHBYNoV3TUIwPeDEOQ09YHPYsrxGG8KwmEtiQQJ8XzfD9DC-RkfycRB9msdt9kuBYNA0Dxlg0Adgm5bd8Og8McwPABlGN2DAEiuDYEg1yaJUt0gmSszk2CviUgZVJndSl0ISjL1LGjJFvSsGOrBAtC0Q4tFEc5DHE-YPI0XjTA9NRDCdI4rhEvR9HrEKIMefSwzHYVjOUsyEIszT0KTFMcLTOL1QMxLcxMlS1I0qyixs017Loy1GNc9zPMdHy-ICoLDDZehzg0Wx3z4vtbkkvD8oS-cBAgegIHFWAwHYBTlzAXpBnoYoPkmzhjPmxaS0EZAAEkFIAFQwAA5AB9A6AHlTsnAAJABBY6AHEMAU8t8UcolnOE-9fLWZ9NEsAwgtc9ttG6p0fQsQCNFitVMxGoixomqaZrmugtsUZbVqNDb0cGQQslIEU7qO07iYOu6FIwA7Tqp5AMEnA7dou463rvJyH1pETOssQx-u0Lwtm43Yzjtbz1jZGwDg2Ab7j04a90RyBkZjabZs2paVt4NaUjRhb8fJynqdpjB6cZ5mzoAJRey7rdO1I7t25AsmttmPqtTmEG+nm-usAHBba9rOp67trGffz-Nh4cCJgxFlbWrg1b1jHmDiCA6AJomSYwMmSaNmm6YZpmWbd+jPs9s5PFff19jWZsDH2IWdk7MP7UdUx-GbdxTGbKOoIK0b45R9W8ZLNOM8NqmC9NouLdO63Douu2Hadl2MFL2rnMr-8jHZWvjEFxvgcZehTn0fZIr48WYcG6SFcI+SkYTpONbHxgcB1qNdTjLSN2VIb4aK0fkPVWqNX6Y3fp-PM39CwYgvNia81V3plw9ioGsbI1CvnfJ5SwtdTB4OBmoF83k+KmHcCcNyN85Z5UAQ-ccIDE5gNHhAj+ONoExj1PGQgAIspYVTAAkcdC47jWfkw-Wb9WHrXYQWGU1kEF2XNCgxib52RYLZL9PBBDhadncPQEwfF-BXGWKIBYfd4pAPoSI4eyclqQLYcVVKMYyq-x6P-O+tDY5JAYS-Zhqc7FSIcaVSyciSxVUUZvT2KjMGsRwQcJ8Wjm7sUwdYEOlgnxOk5LfeWHjDLCJVowke4iWFQMCeZZxmVMLYVwu4wRnj+DeLESnJgkjdYpSCSQEJ1EN73jQQgFR+h6TnGsL5TQQlz5BWCnMEwEsTGeSipk6hcNam5K8eNXoR4kKPQoO-WATBWCDwgIIXax1dpMzuntAAWjnScLMDqWwusgU6j17mE1OndUgu1ukc16bYQ4ngyHuDDs2ICgVtFrB+syICfse5QxsGY++dSkbrIQnOLZqAdnjzAIIQ2p0JS7QwAAdVOoTcmGARRfPLr098T5T6+T4s6RYWhvIen0J5O0TLgpkPWN3FY8KcmFXqWsjZeA0UYuoOnLFJLs7XVufcx5pAHqm1erRZBESqWdh0EsWu-kiG1zWEFDQJxdAbHaqIYZVxnQSUWdHWSAqkXCtFTgXZ-i4LCpPKhFxcC3HZOWXa5WyLSKOudS0r4JFjwoTPBVeRFLUHTF+UcJ8ZCPAwpBUFPi7kNjnGlm+c1fLfUHPoAG1F2ynXNKgWGpC7qVyCAqcmPhOUBExxWYKwtDqS3BvLW6iNFEo2hMQeEnpcbz4JoBcm4FZC01V0NWoHuYd8F9SCJJZgqAIBwGUI26CA7vnTAALSgp2Duu0KTj0npPVFPNTaWB+ogFuyl0wj7aI0AMwFBxOx8ydG+C9trRptB+LkW9saazOmNf5aZ+CfTMjai+PYngjCdgOJ5bQX6B6Ix1BwuMAHGLuFbPg0+6xTV6EuMyK1UkfVNrta6lFlbu2YecsYAZjciGms8qyVlhhfxRQ7F2KW58Fh+2QwjR+rTTLtMILRz22HtHtRYhyN87gpZMoExY4R4nensR0E6PQz5WTsm+h6Tw7ZvIwuZBSZ8stSM0PzUrKxoDCkp1U9MCW9JnQzuGRgvT2ilhTMNSk8+axOz7CU0I1Z+SfFFNTlrcV9jwGoPZnexA5hiEue0+5kSx9-xgxScJXiWqFkWaWeRgtoi7NLXFXQBz6gjDPq8H7KkAkbDpd0DYfs6wTGdiC4ihpJWJFQPzJwiruwRIMf8rxXyzZmSdmPi+VyfFJawfEoYDrzan7WJi2W+xbSymWQG4Y0wnURvNYg0yiZ-ksHn20LxXsIUSMbpQ8AoVVGRXtoG05zTrmdOaDS9oshL5hkUL9D3KKVD8s2ru5Y1tj2g17OvQNtYcwZ2smy+Qp8WggrOnbCHYCLmNX6CW9eiHgb22YoG2cDqrJWR+Dwf6DQaaTG6GluJNy13nR44LUWzZROXWhq7eRAbiXnNabc7pr7zdDV7cQ+1CbthnyBkXUAA */ - createMachine( - { - context: initialContext, - predictableActionArguments: true, - id: 'LogExplorerProfile', - initial: 'uninitialized', - states: { - uninitialized: { - always: 'initializingFromUrl', - }, - initializingFromUrl: { - invoke: { - src: 'initializeFromUrl', - onDone: { - target: 'initializingDataView', - actions: ['storeDatasetSelection'], - }, - onError: { - target: 'initializingDataView', - actions: ['notifyDatasetSelectionRestoreFailed'], - }, - }, - }, - initializingDataView: { - invoke: { - src: 'createDataView', - onDone: { - target: 'initializingControlPanels', - }, - onError: { - target: 'initialized', - actions: ['notifyCreateDataViewFailed'], - }, - }, - }, - initializingControlPanels: { - invoke: { - src: 'initializeControlPanels', - onDone: { - target: 'initializingStateContainer', - actions: ['storeControlPanels'], - }, - onError: { - target: 'initializingStateContainer', - }, - }, - }, - initializingStateContainer: { - invoke: { - src: 'updateStateContainer', - onDone: { - target: 'initialized', - }, - onError: { - target: 'initialized', - }, - }, - }, - initialized: { - type: 'parallel', - states: { - datasetSelection: { - initial: 'validatingSelection', - states: { - validatingSelection: { - invoke: { - src: 'validateSelection', - }, - on: { - LISTEN_TO_CHANGES: { - target: 'idle', - }, - UPDATE_DATASET_SELECTION: { - target: 'updatingDataView', - actions: ['storeDatasetSelection'], - }, - DATASET_SELECTION_RESTORE_FAILURE: { - target: 'updatingDataView', - actions: ['notifyDatasetSelectionRestoreFailed'], - }, - }, - }, - idle: { - invoke: { - src: 'listenUrlChange', - }, - on: { - UPDATE_DATASET_SELECTION: { - target: 'updatingDataView', - actions: ['storeDatasetSelection'], - }, - DATASET_SELECTION_RESTORE_FAILURE: { - target: 'updatingDataView', - actions: ['notifyDatasetSelectionRestoreFailed'], - }, - }, - }, - updatingDataView: { - invoke: { - src: 'createDataView', - onDone: { - target: 'updatingStateContainer', - }, - onError: { - target: 'updatingStateContainer', - actions: ['notifyCreateDataViewFailed'], - }, - }, - }, - updatingStateContainer: { - invoke: { - src: 'updateStateContainer', - onDone: { - target: 'idle', - actions: ['notifyDataViewUpdate'], - }, - onError: { - target: 'idle', - actions: ['notifyCreateDataViewFailed'], - }, - }, - }, - }, - }, - controlGroups: { - initial: 'uninitialized', - states: { - uninitialized: { - on: { - INITIALIZE_CONTROL_GROUP_API: { - target: 'idle', - cond: 'controlGroupAPIExists', - actions: ['storeControlGroupAPI'], - }, - }, - }, - idle: { - invoke: { - src: 'subscribeControlGroup', - }, - on: { - DATA_VIEW_UPDATED: { - target: 'updatingControlPanels', - }, - UPDATE_CONTROL_PANELS: { - target: 'updatingControlPanels', - }, - }, - }, - updatingControlPanels: { - invoke: { - src: 'updateControlPanels', - onDone: { - target: 'idle', - actions: ['storeControlPanels'], - }, - onError: { - target: 'idle', - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - actions: { - storeDatasetSelection: actions.assign((_context, event) => - 'data' in event && isDatasetSelection(event.data) - ? { - datasetSelection: event.data, - } - : {} - ), - storeControlGroupAPI: actions.assign((_context, event) => - 'controlGroupAPI' in event - ? { - controlGroupAPI: event.controlGroupAPI, - } - : {} - ), - storeControlPanels: actions.assign((_context, event) => - 'data' in event && ControlPanelRT.is(event.data) - ? { - controlPanels: event.data, - } - : {} - ), - notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'), - }, - guards: { - controlGroupAPIExists: (_context, event) => { - return 'controlGroupAPI' in event && event.controlGroupAPI != null; - }, - }, - } - ); - -export interface LogExplorerProfileStateMachineDependencies { - initialContext?: LogExplorerProfileContext; - datasetsClient: IDatasetsClient; - stateContainer: DiscoverStateContainer; - toasts: IToasts; -} - -export const createLogExplorerProfileStateMachine = ({ - initialContext = DEFAULT_CONTEXT, - datasetsClient, - stateContainer, - toasts, -}: LogExplorerProfileStateMachineDependencies) => - createPureLogExplorerProfileStateMachine(initialContext).withConfig({ - actions: { - notifyCreateDataViewFailed: createCreateDataViewFailedNotifier(toasts), - notifyDatasetSelectionRestoreFailed: createDatasetSelectionRestoreFailedNotifier(toasts), - }, - services: { - createDataView: createAndSetDataView({ stateContainer }), - initializeFromUrl: initializeFromUrl({ stateContainer }), - initializeControlPanels: initializeControlPanels({ stateContainer }), - listenUrlChange: listenUrlChange({ stateContainer }), - subscribeControlGroup: subscribeControlGroup({ stateContainer }), - updateControlPanels: updateControlPanels({ stateContainer }), - updateStateContainer: updateStateContainer({ stateContainer }), - validateSelection: validateSelection({ datasetsClient }), - }, - }); - -export const initializeLogExplorerProfileStateService = ( - deps: LogExplorerProfileStateMachineDependencies -) => { - const machine = createLogExplorerProfileStateMachine(deps); - - return interpret(machine).start(); -}; - -export type LogExplorerProfileStateService = InterpreterFrom< - typeof createLogExplorerProfileStateMachine ->; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts deleted file mode 100644 index fe4323fac0cd4..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts +++ /dev/null @@ -1,128 +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 * as rt from 'io-ts'; -import { ControlGroupAPI } from '@kbn/controls-plugin/public'; -import { DoneInvokeEvent } from 'xstate'; -import type { DatasetEncodingError, DatasetSelection } from '../../../../common/dataset_selection'; - -export interface WithDatasetSelection { - datasetSelection: DatasetSelection; -} - -export interface WithControlPanelGroupAPI { - controlGroupAPI: ControlGroupAPI; -} - -export interface WithControlPanels { - controlPanels: ControlPanels; -} - -export type DefaultLogExplorerProfileState = WithDatasetSelection; - -export type LogExplorerProfileTypeState = - | { - value: 'uninitialized'; - context: WithDatasetSelection; - } - | { - value: 'initializingFromUrl'; - context: WithDatasetSelection; - } - | { - value: 'initializingDataView'; - context: WithDatasetSelection; - } - | { - value: 'initializingControlPanels'; - context: WithDatasetSelection; - } - | { - value: 'initializingStateContainer'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.validatingSelection'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.idle'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.updatingDataView'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.updatingStateContainer'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.controlGroups.uninitialized'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.controlGroups.idle'; - context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels; - } - | { - value: 'initialized.controlGroups.updatingControlPanels'; - context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels; - }; - -export type LogExplorerProfileContext = LogExplorerProfileTypeState['context']; - -export type LogExplorerProfileStateValue = LogExplorerProfileTypeState['value']; - -export type LogExplorerProfileEvent = - | { - type: 'LISTEN_TO_CHANGES'; - } - | { - type: 'UPDATE_DATASET_SELECTION'; - data: DatasetSelection; - } - | { - type: 'DATASET_SELECTION_RESTORE_FAILURE'; - } - | { - type: 'INITIALIZE_CONTROL_GROUP_API'; - controlGroupAPI: ControlGroupAPI | undefined; - } - | { - type: 'UPDATE_CONTROL_PANELS'; - controlPanels: ControlPanels | null; - } - | DoneInvokeEvent - | DoneInvokeEvent - | DoneInvokeEvent - | DoneInvokeEvent - | DoneInvokeEvent; - -const PanelRT = rt.type({ - order: rt.number, - width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]), - grow: rt.boolean, - type: rt.string, - explicitInput: rt.intersection([ - rt.type({ id: rt.string }), - rt.partial({ - dataViewId: rt.string, - fieldName: rt.string, - title: rt.union([rt.string, rt.undefined]), - selectedOptions: rt.array(rt.string), - }), - ]), -}); - -export const ControlPanelRT = rt.record(rt.string, PanelRT); - -export type ControlPanels = rt.TypeOf; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts deleted file mode 100644 index b84cce4dbf2cf..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts +++ /dev/null @@ -1,284 +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 { InvokeCreator } from 'xstate'; -import { pick, mapValues } from 'lodash'; -import deepEqual from 'fast-deep-equal'; -import { DiscoverAppState, DiscoverStateContainer } from '@kbn/discover-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; -import { - AllDatasetSelection, - decodeDatasetSelectionId, - hydrateDatasetSelection, - isDatasetSelection, -} from '../../../../common/dataset_selection'; -import { - DATA_GRID_COLUMNS_PREFERENCES, - DATA_GRID_DEFAULT_COLUMNS, - LOG_LEVEL_FIELD, -} from '../../../../common/constants'; -import { - ControlPanelRT, - ControlPanels, - LogExplorerProfileContext, - LogExplorerProfileEvent, -} from './types'; -import { - availableControlPanelFields, - controlPanelConfigs, - CONTROL_PANELS_URL_KEY, -} from './defaults'; - -interface LogExplorerProfileUrlStateDependencies { - stateContainer: DiscoverStateContainer; -} - -export const listenUrlChange = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - (context) => - (send) => { - const unsubscribe = stateContainer.appState.subscribe((nextState) => { - const { index } = nextState; - const prevIndex = stateContainer.appState.getPrevious().index; - - // Preventing update if the index didn't change - if (prevIndex === index) return; - - try { - const datasetSelection = extractDatasetSelectionFromIndex({ index, context }); - - if (isDatasetSelection(datasetSelection)) { - send({ type: 'UPDATE_DATASET_SELECTION', data: datasetSelection }); - } - } catch (error) { - send({ type: 'DATASET_SELECTION_RESTORE_FAILURE' }); - } - }); - - return () => unsubscribe(); - }; - -export const initializeFromUrl = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async (context) => { - const { index } = stateContainer.appState.getState(); - - return extractDatasetSelectionFromIndex({ index, context }); - }; - -export const initializeControlPanels = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async (context) => { - const urlPanels = stateContainer.stateStorage.get(CONTROL_PANELS_URL_KEY); - const controlPanelsWithId = constructControlPanelsWithDataViewId(stateContainer, urlPanels!); - - return controlPanelsWithId; - }; - -const extractDatasetSelectionFromIndex = ({ - index, - context, -}: { - index?: string; - context: LogExplorerProfileContext; -}) => { - // If the index parameter doesn't exists, use initialContext value or fallback to AllDatasetSelection - if (!index) { - return context.datasetSelection ?? AllDatasetSelection.create(); - } - - const rawDatasetSelection = decodeDatasetSelectionId(index); - const datasetSelection = hydrateDatasetSelection(rawDatasetSelection); - - return datasetSelection; -}; - -export const subscribeControlGroup = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - (context) => - (send) => { - if (!('controlGroupAPI' in context)) return; - - const filtersSubscription = context.controlGroupAPI.onFiltersPublished$.subscribe( - (newFilters) => { - stateContainer.internalState.transitions.setCustomFilters(newFilters); - stateContainer.actions.fetchData(); - } - ); - - // Keeps our state in sync with the url changes and makes sure it adheres to correct schema - const urlSubscription = stateContainer.stateStorage - .change$(CONTROL_PANELS_URL_KEY) - .subscribe((controlPanels) => { - if (!deepEqual(controlPanels, context.controlPanels)) { - send({ type: 'UPDATE_CONTROL_PANELS', controlPanels }); - } - }); - - // Keeps the url in sync with the controls state after change - const inputSubscription = context.controlGroupAPI.getInput$().subscribe(({ panels }) => { - if (!deepEqual(panels, context.controlPanels)) { - send({ type: 'UPDATE_CONTROL_PANELS', controlPanels: panels }); - } - }); - - return () => { - filtersSubscription.unsubscribe(); - urlSubscription.unsubscribe(); - inputSubscription.unsubscribe(); - }; - }; - -export const updateControlPanels = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async (context, event) => { - if (!('controlGroupAPI' in context)) return; - - const newControlPanels = - ('controlPanels' in event && event.controlPanels) || context.controlPanels; - const controlPanelsWithId = constructControlPanelsWithDataViewId( - stateContainer, - newControlPanels! - ); - - context.controlGroupAPI.updateInput({ panels: controlPanelsWithId }); - - return controlPanelsWithId; - }; - -export const updateStateContainer = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async () => { - const { breakdownField, columns, grid, rowHeight } = stateContainer.appState.getState(); - const stateUpdates: DiscoverAppState = {}; - - // Update data grid columns list - const shouldSetDefaultColumns = - stateContainer.appState.isEmptyURL() || !columns || columns.length === 0; - if (shouldSetDefaultColumns) { - stateUpdates.columns = DATA_GRID_DEFAULT_COLUMNS; - } - - // Configure DataGrid columns preferences - const initialColumnsPreferences = grid?.columns ?? {}; - stateUpdates.grid = { - columns: { ...DATA_GRID_COLUMNS_PREFERENCES, ...initialColumnsPreferences }, - }; - - // Configure rowHeight preference - stateUpdates.rowHeight = rowHeight ?? ROWS_HEIGHT_OPTIONS.single; - - // Configure breakdown field preference - stateUpdates.breakdownField = breakdownField ?? LOG_LEVEL_FIELD; - - // Finally batch update state app state - stateContainer.appState.update(stateUpdates, true); - }; - -/** - * Utils - */ - -const constructControlPanelsWithDataViewId = ( - stateContainer: DiscoverStateContainer, - newControlPanels: ControlPanels -) => { - const dataView = stateContainer.internalState.getState().dataView!; - - const validatedControlPanels = isValidState(newControlPanels) - ? newControlPanels - : getVisibleControlPanelsConfig(dataView); - - const controlsPanelsWithId = mergeDefaultPanelsWithUrlConfig(dataView, validatedControlPanels!); - - if (!deepEqual(controlsPanelsWithId, stateContainer.stateStorage.get(CONTROL_PANELS_URL_KEY))) { - stateContainer.stateStorage.set( - CONTROL_PANELS_URL_KEY, - cleanControlPanels(controlsPanelsWithId), - { replace: true } - ); - } - - return controlsPanelsWithId; -}; - -const isValidState = (state: ControlPanels | undefined | null): boolean => { - return Object.keys(state ?? {}).length > 0 && ControlPanelRT.is(state); -}; - -const getVisibleControlPanels = (dataView: DataView | undefined) => - availableControlPanelFields.filter( - (panelKey) => dataView?.fields.getByName(panelKey) !== undefined - ); - -export const getVisibleControlPanelsConfig = (dataView?: DataView) => { - return getVisibleControlPanels(dataView).reduce((panelsMap, panelKey) => { - const config = controlPanelConfigs[panelKey]; - - return { ...panelsMap, [panelKey]: config }; - }, {} as ControlPanels); -}; - -const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId: string = '') => { - return mapValues(controlPanels, (controlPanelConfig) => ({ - ...controlPanelConfig, - explicitInput: { ...controlPanelConfig.explicitInput, dataViewId }, - })); -}; - -const cleanControlPanels = (controlPanels: ControlPanels) => { - return mapValues(controlPanels, (controlPanelConfig) => { - const { explicitInput } = controlPanelConfig; - const { dataViewId, ...rest } = explicitInput; - return { ...controlPanelConfig, explicitInput: rest }; - }); -}; - -const mergeDefaultPanelsWithUrlConfig = (dataView: DataView, urlPanels: ControlPanels) => { - // Get default panel configs from existing fields in data view - const visiblePanels = getVisibleControlPanelsConfig(dataView); - - // Get list of panel which can be overridden to avoid merging additional config from url - const existingKeys = Object.keys(visiblePanels); - const controlPanelsToOverride = pick(urlPanels, existingKeys); - - // Merge default and existing configs and add dataView.id to each of them - return addDataViewIdToControlPanels( - { ...visiblePanels, ...controlPanelsToOverride }, - dataView.id - ); -}; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts deleted file mode 100644 index c7907cb358e0c..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts +++ /dev/null @@ -1,23 +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 { LogExplorerProfileStateService } from './state_machine'; -import { LogExplorerProfileStateValue } from './types'; - -export const waitForState = ( - service: LogExplorerProfileStateService, - targetState: LogExplorerProfileStateValue -) => { - return new Promise((resolve) => { - const { unsubscribe } = service.subscribe((state) => { - if (state.matches(targetState)) { - resolve(state); - unsubscribe(); - } - }); - }); -}; diff --git a/x-pack/plugins/log_explorer/public/types.ts b/x-pack/plugins/log_explorer/public/types.ts index e07b8fdb14b3f..32fcf5315e27c 100644 --- a/x-pack/plugins/log_explorer/public/types.ts +++ b/x-pack/plugins/log_explorer/public/types.ts @@ -6,18 +6,22 @@ */ import type { ComponentType } from 'react'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public'; -import { SharePluginSetup } from '@kbn/share-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { LogExplorerLocators } from '../common/locators'; +import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { LogExplorerLocators } from '../common/locators'; import type { LogExplorerProps } from './components/log_explorer'; +import type { CreateLogExplorerController } from './controller'; export interface LogExplorerPluginSetup { locators: LogExplorerLocators; } export interface LogExplorerPluginStart { LogExplorer: ComponentType; + createLogExplorerController: CreateLogExplorerController; } export interface LogExplorerSetupDeps { @@ -30,4 +34,6 @@ export interface LogExplorerStartDeps { dataViews: DataViewsPublicPluginStart; discover: DiscoverStart; fieldFormats: FieldFormatsStart; + navigation: NavigationPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; } diff --git a/x-pack/plugins/log_explorer/public/utils/convert_discover_app_state.ts b/x-pack/plugins/log_explorer/public/utils/convert_discover_app_state.ts new file mode 100644 index 0000000000000..dea02a0bec002 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/utils/convert_discover_app_state.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryState } from '@kbn/data-plugin/public'; +import { DiscoverAppState } from '@kbn/discover-plugin/public'; +import { cloneDeep } from 'lodash'; +import { + ChartDisplayOptions, + DisplayOptions, + GridColumnDisplayOptions, + GridRowsDisplayOptions, +} from '../../common'; + +export const getGridColumnDisplayOptionsFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): GridColumnDisplayOptions[] | undefined => + discoverAppState.columns?.map((field) => ({ + field, + width: discoverAppState.grid?.columns?.[field]?.width, + })); + +export const getGridRowsDisplayOptionsFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): Partial => ({ + ...(discoverAppState.rowHeight != null ? { rowHeight: discoverAppState.rowHeight } : {}), + ...(discoverAppState.rowsPerPage != null ? { rowsPerPage: discoverAppState.rowsPerPage } : {}), +}); + +export const getChartDisplayOptionsFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): Partial => ({ + breakdownField: discoverAppState.breakdownField ?? null, +}); + +export const getQueryStateFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): QueryState => ({ + query: discoverAppState.query, + filters: discoverAppState.filters, +}); + +export const getDiscoverAppStateFromContext = ( + displayOptions: DisplayOptions & QueryState +): Partial => ({ + breakdownField: displayOptions.chart.breakdownField ?? undefined, + columns: getDiscoverColumnsFromDisplayOptions(displayOptions), + grid: getDiscoverGridFromDisplayOptions(displayOptions), + rowHeight: displayOptions.grid.rows.rowHeight, + rowsPerPage: displayOptions.grid.rows.rowsPerPage, + query: cloneDeep(displayOptions.query), + filters: cloneDeep(displayOptions.filters), +}); + +export const getDiscoverColumnsFromDisplayOptions = ( + displayOptions: DisplayOptions +): DiscoverAppState['columns'] => displayOptions.grid.columns.map(({ field }) => field); + +export const getDiscoverGridFromDisplayOptions = ( + displayOptions: DisplayOptions +): DiscoverAppState['grid'] => ({ + columns: displayOptions.grid.columns.reduce< + NonNullable['columns']> + >((gridColumns, { field, width }) => { + if (width != null) { + gridColumns[field] = { width }; + } + return gridColumns; + }, {}), +}); diff --git a/x-pack/plugins/log_explorer/tsconfig.json b/x-pack/plugins/log_explorer/tsconfig.json index a36d764cbee56..b236e7f03ced3 100644 --- a/x-pack/plugins/log_explorer/tsconfig.json +++ b/x-pack/plugins/log_explorer/tsconfig.json @@ -3,31 +3,43 @@ "compilerOptions": { "outDir": "target/types" }, - "include": ["../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*", ".storybook/**/*.tsx"], + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + ".storybook/**/*.tsx" + ], "kbn_references": [ + "@kbn/controls-plugin", "@kbn/core", + "@kbn/core-application-browser", + "@kbn/core-http-browser", + "@kbn/core-ui-settings-browser", + "@kbn/custom-icons", + "@kbn/data-plugin", + "@kbn/data-views-plugin", + "@kbn/deeplinks-observability", "@kbn/discover-plugin", + "@kbn/discover-utils", + "@kbn/elastic-agent-utils", + "@kbn/embeddable-plugin", + "@kbn/es-query", + "@kbn/field-formats-plugin", + "@kbn/fleet-plugin", "@kbn/i18n", "@kbn/i18n-react", - "@kbn/fleet-plugin", "@kbn/io-ts-utils", - "@kbn/data-views-plugin", - "@kbn/rison", - "@kbn/controls-plugin", - "@kbn/embeddable-plugin", - "@kbn/es-query", "@kbn/kibana-react-plugin", - "@kbn/data-plugin", - "@kbn/unified-field-list", - "@kbn/core-application-browser", + "@kbn/kibana-utils-plugin", + "@kbn/navigation-plugin", "@kbn/share-plugin", "@kbn/unified-data-table", - "@kbn/core-ui-settings-browser", - "@kbn/discover-utils", - "@kbn/deeplinks-observability", - "@kbn/field-formats-plugin", - "@kbn/custom-icons", - "@kbn/elastic-agent-utils" + "@kbn/unified-field-list", + "@kbn/unified-search-plugin", + "@kbn/xstate-utils" ], - "exclude": ["target/**/*"] + "exclude": [ + "target/**/*" + ] } diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/budget_consumed.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/budget_consumed.tsx index 85806a8b8d4f5..005cefec1e30f 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/budget_consumed.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/budget_consumed.tsx @@ -8,7 +8,6 @@ import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ChangeEvent, useState } from 'react'; -import numeral from '@elastic/numeral'; interface Props { initialBurnRate?: number; @@ -28,6 +27,7 @@ export function BudgetConsumed({ const [budgetConsumed, setBudgetConsumed] = useState( ((initialBurnRate * longLookbackWindowInHours) / sloTimeWindowInHours) * 100 ); + const [formattedValue, setFormattedValue] = useState(budgetConsumed.toFixed(2)); const hasError = errors !== undefined && errors.length > 0; const onBudgetConsumedChanged = (event: ChangeEvent) => { @@ -60,8 +60,15 @@ export function BudgetConsumed({ step={0.01} min={0.01} max={100} - value={numeral(budgetConsumed).format('0[.0]')} - onChange={(event) => onBudgetConsumedChanged(event)} + value={formattedValue} + onChange={(event) => { + onBudgetConsumedChanged(event); + setFormattedValue(event.target.value); + }} + onBlur={(event) => { + const value = event.target.value; + setFormattedValue(Number(value).toFixed(2)); + }} data-test-subj="budgetConsumed" /> diff --git a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx index ac7b92950fc96..1e76ddec456e2 100644 --- a/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx +++ b/x-pack/plugins/observability/public/components/burn_rate_rule_editor/burn_rate.tsx @@ -8,7 +8,6 @@ import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ChangeEvent, useState } from 'react'; -import numeral from '@elastic/numeral'; interface Props { initialBurnRate?: number; @@ -20,6 +19,7 @@ interface Props { export function BurnRate({ onChange, initialBurnRate = 1, maxBurnRate, errors }: Props) { const [burnRate, setBurnRate] = useState(initialBurnRate); const hasError = errors !== undefined && errors.length > 0; + const [formattedValue, setFormattedValue] = useState(burnRate.toFixed(2)); const onBurnRateChange = (event: ChangeEvent) => { const value = Number(event.target.value); @@ -51,8 +51,15 @@ export function BurnRate({ onChange, initialBurnRate = 1, maxBurnRate, errors }: step={0.01} min={0.01} max={maxBurnRate} - value={numeral(burnRate).format('0[.0]')} - onChange={(event) => onBurnRateChange(event)} + value={formattedValue} + onChange={(event) => { + onBurnRateChange(event); + setFormattedValue(event.target.value); + }} + onBlur={(event) => { + const value = event.target.value; + setFormattedValue(Number(value).toFixed(2)); + }} data-test-subj="burnRate" /> diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx index 8d4d45a08d709..cd3459975d011 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx @@ -128,6 +128,8 @@ describe('SLO Edit Page', () => { const mockCreate = jest.fn(); const mockUpdate = jest.fn(); + const history = createBrowserHistory(); + beforeEach(() => { jest.clearAllMocks(); mockKibana(); @@ -136,9 +138,8 @@ describe('SLO Edit Page', () => { jest.spyOn(console, 'warn').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); - const history = createBrowserHistory(); history.replace(''); - jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history); + jest.spyOn(Router, 'useHistory').mockReturnValue(history); useFetchDataViewsMock.mockReturnValue({ isLoading: false, @@ -256,11 +257,9 @@ describe('SLO Edit Page', () => { it('prefills the form with values from URL', () => { jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined }); - const history = createBrowserHistory(); history.replace( '/slos/create?_a=(indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration))' ); - jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history); jest .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); @@ -336,11 +335,9 @@ describe('SLO Edit Page', () => { const slo = buildSlo({ id: '123' }); jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' }); - const history = createBrowserHistory(); history.push( '/slos/123/edit?_a=(name:%27updated-name%27,indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration),objective:(target:0.92))' ); - jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history); jest .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); diff --git a/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts b/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts index fc53bf1e7181b..5e6f50398fc95 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_search_client.test.ts @@ -74,10 +74,7 @@ describe('Summary Search Client', () => { const SLO_ID3 = 'slo-three'; const SLO_ID4 = 'slo-four'; const SLO_ID5 = 'slo-five'; - esClientMock.count.mockResolvedValue({ - count: 8, - _shards: { failed: 0, successful: 1, total: 1 }, - }); + esClientMock.search.mockResolvedValue({ took: 0, timed_out: false, @@ -89,7 +86,7 @@ describe('Summary Search Client', () => { }, hits: { total: { - value: 6, + value: 8, relation: 'eq', }, max_score: 1, diff --git a/x-pack/plugins/observability/server/services/slo/summary_search_client.ts b/x-pack/plugins/observability/server/services/slo/summary_search_client.ts index 2361e8dd3678e..a145a76aa729d 100644 --- a/x-pack/plugins/observability/server/services/slo/summary_search_client.ts +++ b/x-pack/plugins/observability/server/services/slo/summary_search_client.ts @@ -9,6 +9,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ALL_VALUE } from '@kbn/slo-schema'; import { assertNever } from '@kbn/std'; import _ from 'lodash'; +import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/slo/constants'; import { SLOId, Status, Summary } from '../../domain/models'; import { toHighPrecision } from '../../utils/number'; @@ -67,17 +68,9 @@ export class DefaultSummarySearchClient implements SummarySearchClient { pagination: Pagination ): Promise> { try { - const { count: total } = await this.esClient.count({ - index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - query: getElastichsearchQueryOrThrow(kqlQuery), - }); - - if (total === 0) { - return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] }; - } - const summarySearch = await this.esClient.search({ index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + track_total_hits: true, query: getElastichsearchQueryOrThrow(kqlQuery), sort: { // non-temp first, then temp documents @@ -92,6 +85,11 @@ export class DefaultSummarySearchClient implements SummarySearchClient { size: pagination.perPage * 2, // twice as much as we return, in case they are all duplicate temp/non-temp summary }); + const total = (summarySearch.hits.total as SearchTotalHits).value ?? 0; + if (total === 0) { + return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] }; + } + const [tempSummaryDocuments, summaryDocuments] = _.partition( summarySearch.hits.hits, (doc) => !!doc._source?.isTempDoc diff --git a/x-pack/plugins/observability_log_explorer/common/index.ts b/x-pack/plugins/observability_log_explorer/common/index.ts index f7639f742e67e..f94c83117e7e8 100644 --- a/x-pack/plugins/observability_log_explorer/common/index.ts +++ b/x-pack/plugins/observability_log_explorer/common/index.ts @@ -10,3 +10,5 @@ export { SingleDatasetLocatorDefinition, AllDatasetsLocatorDefinition, } from './locators'; +export { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from './url_schema'; +export { deepCompactObject } from './utils/deep_compact_object'; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts b/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts index 17c8b2ae02047..41cf67546c78d 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts @@ -23,11 +23,10 @@ export class AllDatasetsLocatorDefinition implements LocatorDefinition { const { useHash } = this.deps; - const index = AllDatasetSelection.create().toDataviewSpec().id; return constructLocatorPath({ + datasetSelection: AllDatasetSelection.create().toPlainSelection(), locatorParams: params, - index, useHash, }); }; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts b/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts index d26ff4e133ea6..ff12691e8c2fd 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts @@ -5,13 +5,11 @@ * 2.0. */ -import { FilterStateStore } from '@kbn/es-query'; -import { getStatesFromKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; import { AllDatasetsLocatorParams, SingleDatasetLocatorParams, } from '@kbn/deeplinks-observability/locators'; -import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability'; import { AllDatasetsLocatorDefinition } from './all_datasets/all_datasets_locator'; import { SingleDatasetLocatorDefinition } from './single_dataset'; import { DatasetLocatorDependencies } from './types'; @@ -38,8 +36,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation({}); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),v:1)', state: {}, }); }); @@ -53,8 +51,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),time:(from:now-30m,to:now),v:1)', state: {}, }); }); @@ -70,8 +68,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,query:(language:kuery,query:foo))', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),query:(language:kuery,query:foo),v:1)', state: {}, }); }); @@ -88,29 +86,28 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),refreshInterval:(pause:!f,value:666),v:1)', state: {}, }); }); - it('should allow specifiying columns and sort', async () => { + it('should allow specifiying columns', async () => { const params: AllDatasetsLocatorParams = { columns: ['_source'], - sort: [['timestamp, asc']] as string[][], }; const { allDatasetsLocator } = await setup(); const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,sort:!(!('timestamp,%20asc')))`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selectionType:all),v:1)`, state: {}, }); }); - it('should allow specifiying filters', async () => { + it('should allow specifying filters', async () => { const params: AllDatasetsLocatorParams = { filters: [ { @@ -119,9 +116,6 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.APP_STATE, - }, }, { meta: { @@ -129,47 +123,16 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.GLOBAL_STATE, - }, }, ], }; const { allDatasetsLocator } = await setup(); - const { path } = await allDatasetsLocator.getLocation(params); - - const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: false }); + const location = await allDatasetsLocator.getLocation(params); - expect(_a).toEqual({ - filters: [ - { - $state: { - store: 'appState', - }, - meta: { - alias: 'foo', - disabled: false, - negate: false, - }, - }, - ], - index: 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA', - }); - expect(_g).toEqual({ - filters: [ - { - $state: { - store: 'globalState', - }, - meta: { - alias: 'bar', - disabled: false, - negate: false, - }, - }, - ], - }); + expect(location.path).toMatchInlineSnapshot( + `"/?pageState=(datasetSelection:(selectionType:all),filters:!((meta:(alias:foo,disabled:!f,negate:!f)),(meta:(alias:bar,disabled:!f,negate:!f))),v:1)"` + ); }); }); @@ -184,8 +147,8 @@ describe('Observability Logs Explorer Locators', () => { }); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`, state: {}, }); }); @@ -201,8 +164,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),time:(from:now-30m,to:now),v:1)`, state: {}, }); }); @@ -221,8 +184,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,query:(language:kuery,query:foo))`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),query:(language:kuery,query:foo),v:1)`, state: {}, }); }); @@ -241,26 +204,25 @@ describe('Observability Logs Explorer Locators', () => { const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),refreshInterval:(pause:!f,value:666),v:1)`, state: {}, }); }); - it('should allow specifiying columns and sort', async () => { + it('should allow specifiying columns', async () => { const params: SingleDatasetLocatorParams = { integration, dataset, columns: ['_source'], - sort: [['timestamp, asc']] as string[][], }; const { singleDatasetLocator } = await setup(); const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,sort:!(!('timestamp,%20asc')))`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`, state: {}, }); }); @@ -276,9 +238,6 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.APP_STATE, - }, }, { meta: { @@ -286,48 +245,16 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.GLOBAL_STATE, - }, }, ], }; const { singleDatasetLocator } = await setup(); - const { path } = await singleDatasetLocator.getLocation(params); - - const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: false }); + const location = await singleDatasetLocator.getLocation(params); - expect(_a).toEqual({ - filters: [ - { - $state: { - store: 'appState', - }, - meta: { - alias: 'foo', - disabled: false, - negate: false, - }, - }, - ], - index: - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA', - }); - expect(_g).toEqual({ - filters: [ - { - $state: { - store: 'globalState', - }, - meta: { - alias: 'bar', - disabled: false, - negate: false, - }, - }, - ], - }); + expect(location.path).toMatchInlineSnapshot( + `"/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),filters:!((meta:(alias:foo,disabled:!f,negate:!f)),(meta:(alias:bar,disabled:!f,negate:!f))),v:1)"` + ); }); }); }); diff --git a/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts b/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts index 632d0d8d93de6..7f1ed847a9094 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts @@ -35,11 +35,9 @@ export class SingleDatasetLocatorDefinition }, }); - const index = unresolvedDatasetSelection.toDataviewSpec().id; - return constructLocatorPath({ + datasetSelection: unresolvedDatasetSelection.toPlainSelection(), locatorParams: params, - index, useHash, }); }; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/types.ts b/x-pack/plugins/observability_log_explorer/common/locators/types.ts index a181fdce10a02..25228c1f56c04 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/types.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/types.ts @@ -5,16 +5,6 @@ * 2.0. */ -import { AggregateQuery, Filter, Query } from '@kbn/es-query'; - -export interface AppState { - index?: string; - query?: Query | AggregateQuery; - filters?: Filter[]; - columns?: string[]; - sort?: string[][]; -} - export interface DatasetLocatorDependencies { useHash: boolean; } diff --git a/x-pack/plugins/observability_log_explorer/common/locators/utils/construct_locator_path.ts b/x-pack/plugins/observability_log_explorer/common/locators/utils/construct_locator_path.ts new file mode 100644 index 0000000000000..57c5cb018a77d --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/locators/utils/construct_locator_path.ts @@ -0,0 +1,89 @@ +/* + * 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 { + DatasetLocatorParams, + FilterControls, + ListFilterControl, +} from '@kbn/deeplinks-observability/locators'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; +import { + AvailableControlPanels, + availableControlsPanels, + DatasetSelectionPlain, +} from '@kbn/log-explorer-plugin/common'; +import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; +import { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from '../../url_schema'; +import { deepCompactObject } from '../../utils/deep_compact_object'; + +type ControlsPageState = NonNullable; + +interface LocatorPathConstructionParams { + datasetSelection: DatasetSelectionPlain; + locatorParams: DatasetLocatorParams; + useHash: boolean; +} + +export const constructLocatorPath = async (params: LocatorPathConstructionParams) => { + const { + datasetSelection, + locatorParams: { filterControls, filters, query, refreshInterval, timeRange, columns, origin }, + useHash, + } = params; + + const pageState = urlSchemaV1.urlSchemaRT.encode( + deepCompactObject({ + v: 1, + datasetSelection, + filters, + query, + refreshInterval, + time: timeRange, + columns: columns?.map((field) => ({ field })), + controls: getControlsPageStateFromFilterControlsParams(filterControls ?? {}), + }) + ); + + const path = setStateToKbnUrl( + OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, + pageState, + { useHash, storeInHashQuery: false }, + '/' + ); + + return { + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path, + state: { + ...(origin ? { origin } : {}), + }, + }; +}; + +const getControlsPageStateFromFilterControlsParams = ( + filterControls: FilterControls +): ControlsPageState => ({ + ...(filterControls.namespace != null + ? getFilterControlPageStateFromListFilterControlsParams( + availableControlsPanels.NAMESPACE, + filterControls.namespace + ) + : {}), +}); + +const getFilterControlPageStateFromListFilterControlsParams = ( + controlId: AvailableControlPanels[keyof AvailableControlPanels], + listFilterControl: ListFilterControl +): ControlsPageState => ({ + [controlId]: { + mode: listFilterControl.mode, + selection: { + type: 'options', + selectedOptions: listFilterControl.values, + }, + }, +}); diff --git a/x-pack/plugins/observability_log_explorer/common/locators/utils/helpers.ts b/x-pack/plugins/observability_log_explorer/common/locators/utils/helpers.ts deleted file mode 100644 index 5a93709e76522..0000000000000 --- a/x-pack/plugins/observability_log_explorer/common/locators/utils/helpers.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; -import { DatasetLocatorParams } from '@kbn/deeplinks-observability/locators'; -import { AppState } from '../types'; - -interface LocatorPathCosntructionParams { - locatorParams: DatasetLocatorParams; - index: string; - useHash: boolean; -} - -export const constructLocatorPath = async (params: LocatorPathCosntructionParams) => { - const { isFilterPinned } = await import('@kbn/es-query'); - - const { - locatorParams: { filters, query, refreshInterval, timeRange, columns, sort, origin }, - index, - useHash, - } = params; - const appState: AppState = {}; - const queryState: GlobalQueryStateFromUrl = {}; - - // App state - if (index) appState.index = index; - if (query) appState.query = query; - if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f)); - if (columns) appState.columns = columns; - if (sort) appState.sort = sort; - - // Global State - if (timeRange) queryState.time = timeRange; - if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f)); - if (refreshInterval) queryState.refreshInterval = refreshInterval; - - let path = '/'; - - if (Object.keys(queryState).length) { - path = setStateToKbnUrl( - '_g', - queryState, - { useHash, storeInHashQuery: false }, - path - ); - } - - path = setStateToKbnUrl('_a', appState, { useHash, storeInHashQuery: false }, path); - - return { - app: 'observability-log-explorer', - path, - state: { - ...(origin ? { origin } : {}), - }, - }; -}; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts b/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts index 6c315f929b9bb..6e5aad44fbe9a 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './helpers'; +export * from './construct_locator_path'; diff --git a/x-pack/plugins/observability_log_explorer/common/url_schema/common.ts b/x-pack/plugins/observability_log_explorer/common/url_schema/common.ts new file mode 100644 index 0000000000000..fa3b3f72383d1 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/url_schema/common.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY = 'pageState'; diff --git a/x-pack/plugins/observability_log_explorer/common/url_schema/index.ts b/x-pack/plugins/observability_log_explorer/common/url_schema/index.ts new file mode 100644 index 0000000000000..d8f53e47b6058 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/url_schema/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY } from './common'; +export * as urlSchemaV1 from './url_schema_v1'; diff --git a/x-pack/plugins/observability_log_explorer/common/url_schema/url_schema_v1.ts b/x-pack/plugins/observability_log_explorer/common/url_schema/url_schema_v1.ts new file mode 100644 index 0000000000000..c4f611466a4f6 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/url_schema/url_schema_v1.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { availableControlsPanels, datasetSelectionPlainRT } from '@kbn/log-explorer-plugin/common'; +import * as rt from 'io-ts'; + +export const columnRT = rt.intersection([ + rt.strict({ + field: rt.string, + }), + rt.exact( + rt.partial({ + width: rt.number, + }) + ), +]); + +export const columnsRT = rt.array(columnRT); + +export const optionsListControlRT = rt.strict({ + mode: rt.keyof({ + exclude: null, + include: null, + }), + selection: rt.union([ + rt.strict({ + type: rt.literal('exists'), + }), + rt.strict({ + type: rt.literal('options'), + selectedOptions: rt.array(rt.string), + }), + ]), +}); + +export const controlsRT = rt.exact( + rt.partial({ + [availableControlsPanels.NAMESPACE]: optionsListControlRT, + }) +); + +export const filterMetaRT = rt.partial({ + alias: rt.union([rt.string, rt.null]), + disabled: rt.boolean, + negate: rt.boolean, + controlledBy: rt.string, + group: rt.string, + index: rt.string, + isMultiIndex: rt.boolean, + type: rt.string, + key: rt.string, + params: rt.any, + value: rt.any, +}); + +export const filterRT = rt.intersection([ + rt.strict({ + meta: filterMetaRT, + }), + rt.exact( + rt.partial({ + query: rt.UnknownRecord, + }) + ), +]); + +export const filtersRT = rt.array(filterRT); + +const queryRT = rt.union([ + rt.strict({ + language: rt.string, + query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]), + }), + rt.strict({ + sql: rt.string, + }), + rt.strict({ + esql: rt.string, + }), +]); + +const timeRangeRT = rt.intersection([ + rt.strict({ + from: rt.string, + to: rt.string, + }), + rt.exact( + rt.partial({ + mode: rt.keyof({ + absolute: null, + relative: null, + }), + }) + ), +]); + +const refreshIntervalRT = rt.strict({ + pause: rt.boolean, + value: rt.number, +}); + +export const urlSchemaRT = rt.exact( + rt.partial({ + v: rt.literal(1), + breakdownField: rt.union([rt.string, rt.null]), + columns: columnsRT, + datasetSelection: datasetSelectionPlainRT, + filters: filtersRT, + query: queryRT, + refreshInterval: refreshIntervalRT, + rowHeight: rt.number, + rowsPerPage: rt.number, + time: timeRangeRT, + controls: controlsRT, + }) +); + +export type UrlSchema = rt.TypeOf; diff --git a/x-pack/plugins/observability_log_explorer/common/utils/deep_compact_object.ts b/x-pack/plugins/observability_log_explorer/common/utils/deep_compact_object.ts new file mode 100644 index 0000000000000..eed8b1b83e1f4 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/utils/deep_compact_object.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty, isPlainObject, isUndefined } from 'lodash'; + +export const deepCompactObject = >(obj: Value): Value => + Object.fromEntries( + Object.entries(obj) + .map(([key, value]) => [key, isPlainObject(value) ? deepCompactObject(value) : value]) + .filter(([, value]) => !isUndefined(value) && !(isPlainObject(value) && isEmpty(value))) + ); diff --git a/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx index 8a49db7536350..a823ad1a840cd 100644 --- a/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx +++ b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx @@ -10,12 +10,13 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import React from 'react'; import ReactDOM from 'react-dom'; -import { DatasetQualityRoute, ObservablityLogExplorerMainRoute } from '../routes/main'; +import { DatasetQualityRoute, ObservabilityLogExplorerMainRoute } from '../routes/main'; import { ObservabilityLogExplorerAppMountParameters, ObservabilityLogExplorerPluginStart, ObservabilityLogExplorerStartDeps, } from '../types'; +import { KbnUrlStateStorageFromRouterProvider } from '../utils/kbn_url_state_context'; import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; export const renderObservabilityLogExplorer = ( @@ -59,26 +60,25 @@ export const ObservabilityLogExplorerApp = ({ const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( core, plugins, - pluginStart + pluginStart, + appParams ); return ( - - - } - /> - } - /> - - + + + + } /> + } + /> + + + ); diff --git a/x-pack/plugins/observability_log_explorer/public/components/discover_link.tsx b/x-pack/plugins/observability_log_explorer/public/components/discover_link.tsx new file mode 100644 index 0000000000000..2d12de11731c0 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/discover_link.tsx @@ -0,0 +1,111 @@ +/* + * 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 { EuiHeaderLink } from '@elastic/eui'; +import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { DiscoverStart } from '@kbn/discover-plugin/public'; +import { hydrateDatasetSelection } from '@kbn/log-explorer-plugin/common'; +import { getDiscoverColumnsFromDisplayOptions } from '@kbn/log-explorer-plugin/public'; +import { MatchedStateFromActor } from '@kbn/xstate-utils'; +import { useActor } from '@xstate/react'; +import React, { useMemo } from 'react'; +import { discoverLinkTitle } from '../../common/translations'; +import { + ObservabilityLogExplorerService, + useObservabilityLogExplorerPageStateContext, +} from '../state_machines/observability_log_explorer/src'; +import { getRouterLinkProps } from '../utils/get_router_link_props'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; + +export const ConnectedDiscoverLink = React.memo(() => { + const { + services: { discover }, + } = useKibanaContextForPlugin(); + + const [pageState] = useActor(useObservabilityLogExplorerPageStateContext()); + + if (pageState.matches({ initialized: 'validLogExplorerState' })) { + return ; + } else { + return ; + } +}); + +type InitializedPageState = MatchedStateFromActor< + ObservabilityLogExplorerService, + { initialized: 'validLogExplorerState' } +>; + +export const DiscoverLinkForValidState = React.memo( + ({ + discover, + pageState: { + context: { logExplorerState }, + }, + }: { + discover: DiscoverStart; + pageState: InitializedPageState; + }) => { + const discoverLinkParams = useMemo( + () => ({ + breakdownField: logExplorerState.chart.breakdownField ?? undefined, + columns: getDiscoverColumnsFromDisplayOptions(logExplorerState), + filters: logExplorerState.filters, + query: logExplorerState.query, + refreshInterval: logExplorerState.refreshInterval, + timeRange: logExplorerState.time, + dataViewSpec: hydrateDatasetSelection(logExplorerState.datasetSelection).toDataviewSpec(), + }), + [logExplorerState] + ); + + return ; + } +); + +export const DiscoverLinkForUnknownState = React.memo(() => ( + + {discoverLinkTitle} + +)); + +export const DiscoverLink = React.memo( + ({ + discover, + discoverLinkParams, + }: { + discover: DiscoverStart; + discoverLinkParams: DiscoverAppLocatorParams; + }) => { + const discoverUrl = discover.locator?.getRedirectUrl(discoverLinkParams); + + const navigateToDiscover = () => { + discover.locator?.navigate(discoverLinkParams); + }; + + const discoverLinkProps = getRouterLinkProps({ + href: discoverUrl, + onClick: navigateToDiscover, + }); + + return ( + + {discoverLinkTitle} + + ); + } +); diff --git a/x-pack/plugins/observability_log_explorer/public/components/feedback_link.tsx b/x-pack/plugins/observability_log_explorer/public/components/feedback_link.tsx new file mode 100644 index 0000000000000..efd37f8ccd3d4 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/feedback_link.tsx @@ -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 { EuiHeaderLink } from '@elastic/eui'; +import { LOG_EXPLORER_FEEDBACK_LINK } from '@kbn/observability-shared-plugin/common'; +import React from 'react'; +import { feedbackLinkTitle } from '../../common/translations'; + +export const FeedbackLink = React.memo(() => { + return ( + + {feedbackLinkTitle} + + ); +}); diff --git a/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx index c627f0760bb2d..6a76caae25406 100644 --- a/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx +++ b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx @@ -5,74 +5,39 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; -import deepEqual from 'fast-deep-equal'; -import useObservable from 'react-use/lib/useObservable'; -import { type BehaviorSubject, distinctUntilChanged, filter, take } from 'rxjs'; -import styled from '@emotion/styled'; -import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; import { EuiBetaBadge, - EuiButton, EuiHeader, - EuiHeaderLink, EuiHeaderLinks, EuiHeaderSection, EuiHeaderSectionItem, } from '@elastic/eui'; -import { LogExplorerStateContainer } from '@kbn/log-explorer-plugin/public'; -import { - OBSERVABILITY_ONBOARDING_LOCATOR, - ObservabilityOnboardingLocatorParams, -} from '@kbn/deeplinks-observability/locators'; -import { KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; -import { toMountPoint } from '@kbn/react-kibana-mount'; import { css } from '@emotion/react'; -import { LOG_EXPLORER_FEEDBACK_LINK } from '@kbn/observability-shared-plugin/common'; +import styled from '@emotion/styled'; +import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { euiThemeVars } from '@kbn/ui-theme'; import { LogExplorerTabs } from '@kbn/discover-plugin/public'; -import { PluginKibanaContextValue } from '../utils/use_kibana'; -import { - betaBadgeDescription, - betaBadgeTitle, - discoverLinkTitle, - feedbackLinkTitle, - onboardingLinkTitle, -} from '../../common/translations'; -import { getRouterLinkProps } from '../utils/get_router_link_props'; -import { ObservabilityLogExplorerAppMountParameters } from '../types'; - -interface LogExplorerTopNavMenuProps { - setHeaderActionMenu: ObservabilityLogExplorerAppMountParameters['setHeaderActionMenu']; - services: KibanaReactContextValue['services']; - state$: BehaviorSubject; - theme$: ObservabilityLogExplorerAppMountParameters['theme$']; -} - -export const LogExplorerTopNavMenu = ({ - setHeaderActionMenu, - services, - state$, - theme$, -}: LogExplorerTopNavMenuProps) => { - const { serverless } = services; - - return Boolean(serverless) ? ( - - ) : ( - - ); +import React, { useEffect, useState } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { filter, take } from 'rxjs'; +import { betaBadgeDescription, betaBadgeTitle } from '../../common/translations'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; +import { ConnectedDiscoverLink } from './discover_link'; +import { FeedbackLink } from './feedback_link'; +import { ConnectedOnboardingLink } from './onboarding_link'; + +export const LogExplorerTopNavMenu = () => { + const { + services: { serverless }, + } = useKibanaContextForPlugin(); + + return Boolean(serverless) ? : ; }; -const ServerlessTopNav = ({ - services, - state$, -}: Pick) => { +const ServerlessTopNav = () => { + const { services } = useKibanaContextForPlugin(); + return ( @@ -97,38 +62,40 @@ const ServerlessTopNav = ({ - + - + ); }; -const StatefulTopNav = ({ - setHeaderActionMenu, - services, - state$, - theme$, -}: LogExplorerTopNavMenuProps) => { +const StatefulTopNav = () => { + const { + services: { + appParams: { setHeaderActionMenu }, + chrome, + i18n, + theme, + }, + } = useKibanaContextForPlugin(); + /** * Since the breadcrumbsAppendExtension might be set only during a plugin start (e.g. search session) * we retrieve the latest valid extension in order to restore it once we unmount the beta badge. */ const [previousAppendExtension$] = useState(() => - services.chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1)) + chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1)) ); const previousAppendExtension = useObservable(previousAppendExtension$); useEffect(() => { - const { chrome, i18n, theme } = services; - if (chrome) { chrome.setBreadcrumbsAppendExtension({ content: toMountPoint( @@ -161,15 +128,15 @@ const StatefulTopNav = ({ chrome.setBreadcrumbsAppendExtension(previousAppendExtension); } }; - }, [services, previousAppendExtension]); + }, [chrome, i18n, previousAppendExtension, theme]); return ( - + - - + + @@ -177,113 +144,8 @@ const StatefulTopNav = ({ ); }; -const DiscoverLink = React.memo( - ({ services, state$ }: Pick) => { - const discoverLinkParams = useDiscoverLinkParams(state$); - const discoverUrl = services.discover.locator?.getRedirectUrl(discoverLinkParams); - - const navigateToDiscover = () => { - services.discover.locator?.navigate(discoverLinkParams); - }; - - const discoverLinkProps = getRouterLinkProps({ - href: discoverUrl, - onClick: navigateToDiscover, - }); - - return ( - - {discoverLinkTitle} - - ); - } -); - -const OnboardingLink = React.memo(({ services }: Pick) => { - const locator = services.share.url.locators.get( - OBSERVABILITY_ONBOARDING_LOCATOR - ); - - const onboardingUrl = locator?.useUrl({}); - - const navigateToOnboarding = () => { - locator?.navigate({}); - }; - - const onboardingLinkProps = getRouterLinkProps({ - href: onboardingUrl, - onClick: navigateToOnboarding, - }); - - return ( - - {onboardingLinkTitle} - - ); -}); - -const FeedbackLink = React.memo(() => { - return ( - - {feedbackLinkTitle} - - ); -}); - const VerticalRule = styled.span` width: 1px; height: 20px; background-color: ${euiThemeVars.euiColorLightShade}; `; - -const useDiscoverLinkParams = (state$: BehaviorSubject) => { - const { appState, logExplorerState } = useObservable( - state$.pipe( - distinctUntilChanged((prev, curr) => { - if (!prev.appState || !curr.appState) return false; - return deepEqual( - [ - prev.appState.columns, - prev.appState.sort, - prev.appState.filters, - prev.appState.index, - prev.appState.query, - ], - [ - curr.appState.columns, - curr.appState.sort, - curr.appState.filters, - curr.appState.index, - curr.appState.query, - ] - ); - }) - ), - { appState: {}, logExplorerState: {} } - ); - - return { - columns: appState?.columns, - sort: appState?.sort, - filters: appState?.filters, - query: appState?.query, - dataViewSpec: logExplorerState?.datasetSelection?.selection.dataset.toDataviewSpec(), - }; -}; diff --git a/x-pack/plugins/observability_log_explorer/public/components/onboarding_link.tsx b/x-pack/plugins/observability_log_explorer/public/components/onboarding_link.tsx new file mode 100644 index 0000000000000..abdc585c22768 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/onboarding_link.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import { + ObservabilityOnboardingLocatorParams, + OBSERVABILITY_ONBOARDING_LOCATOR, +} from '@kbn/deeplinks-observability/locators'; +import { BrowserUrlService } from '@kbn/share-plugin/public'; +import React from 'react'; +import { onboardingLinkTitle } from '../../common/translations'; +import { getRouterLinkProps } from '../utils/get_router_link_props'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; + +export const ConnectedOnboardingLink = React.memo(() => { + const { + services: { + share: { url }, + }, + } = useKibanaContextForPlugin(); + + return ; +}); + +export const OnboardingLink = React.memo(({ urlService }: { urlService: BrowserUrlService }) => { + const locator = urlService.locators.get( + OBSERVABILITY_ONBOARDING_LOCATOR + ); + + const onboardingUrl = locator?.useUrl({}); + + const navigateToOnboarding = () => { + locator?.navigate({}); + }; + + const onboardingLinkProps = getRouterLinkProps({ + href: onboardingUrl, + onClick: navigateToOnboarding, + }); + + return ( + + {onboardingLinkTitle} + + ); +}); diff --git a/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx b/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx index d128c6e8a7779..5b57333dd8c86 100644 --- a/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx +++ b/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx @@ -7,23 +7,27 @@ import { EuiPageSectionProps } from '@elastic/eui'; import { css } from '@emotion/react'; -import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; import React from 'react'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; export const ObservabilityLogExplorerPageTemplate = ({ children, - observabilityShared, pageProps, }: React.PropsWithChildren<{ - observabilityShared: ObservabilitySharedPluginStart; pageProps?: EuiPageSectionProps; -}>) => ( - - {children} - -); +}>) => { + const { + services: { observabilityShared }, + } = useKibanaContextForPlugin(); + + return ( + + {children} + + ); +}; const fullHeightContentStyles = css` display: flex; diff --git a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx index 53d34a71a7237..7983c6ed39433 100644 --- a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx +++ b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx @@ -13,6 +13,9 @@ import type { LogAIAssistantDocument } from '@kbn/logs-shared-plugin/public'; import React, { useMemo } from 'react'; import { useKibanaContextForPlugin } from '../utils/use_kibana'; +type RenderFlyoutContentCustomization = + Required['flyout']['renderContent']; + const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => { const { services } = useKibanaContextForPlugin(); const { LogAIAssistant } = services.logsShared; @@ -22,19 +25,17 @@ const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => return ; }; -export const renderFlyoutContent: Required['flyout']['renderContent'] = ( - renderPreviousContent, - props -) => { - return ( - <> - {renderPreviousContent()} - - - - - ); -}; +export const renderFlyoutContent: RenderFlyoutContentCustomization = + (renderPreviousContent) => (props) => { + return ( + <> + {renderPreviousContent(props)} + + + + + ); + }; /** * Utils diff --git a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts index a86b47e92cf01..02909efed0d81 100644 --- a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts +++ b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts @@ -5,11 +5,18 @@ * 2.0. */ -import { LogExplorerCustomizations } from '@kbn/log-explorer-plugin/public'; +import { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public'; import { renderFlyoutContent } from './flyout_content'; -export const createLogExplorerCustomizations = (): LogExplorerCustomizations => ({ - flyout: { - renderContent: renderFlyoutContent, - }, -}); +export const createLogExplorerControllerWithCustomizations = + (createLogExplorerController: CreateLogExplorerController): CreateLogExplorerController => + (args) => + createLogExplorerController({ + ...args, + customizations: { + ...args.customizations, + flyout: { + renderContent: renderFlyoutContent, + }, + }, + }); diff --git a/x-pack/plugins/observability_log_explorer/public/plugin.ts b/x-pack/plugins/observability_log_explorer/public/plugin.ts index 5b5a640e2a6ae..6af6484e883b5 100644 --- a/x-pack/plugins/observability_log_explorer/public/plugin.ts +++ b/x-pack/plugins/observability_log_explorer/public/plugin.ts @@ -13,15 +13,14 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/public'; -import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability'; +import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; import { + AllDatasetsLocatorDefinition, ObservabilityLogExplorerLocators, SingleDatasetLocatorDefinition, - AllDatasetsLocatorDefinition, } from '../common/locators'; import { type ObservabilityLogExplorerConfig } from '../common/plugin_config'; import { logExplorerAppTitle } from '../common/translations'; -import { renderObservabilityLogExplorer } from './applications/observability_log_explorer'; import type { ObservabilityLogExplorerAppMountParameters, ObservabilityLogExplorerPluginSetup, @@ -48,7 +47,7 @@ export class ObservabilityLogExplorerPlugin const useHash = core.uiSettings.get('state:storeInSessionStorage'); core.application.register({ - id: OBSERVABILITY_LOG_EXPLORER, + id: OBSERVABILITY_LOG_EXPLORER_APP_ID, title: logExplorerAppTitle, category: DEFAULT_APP_CATEGORIES.observability, euiIconType: 'logoLogging', @@ -59,6 +58,9 @@ export class ObservabilityLogExplorerPlugin keywords: ['logs', 'log', 'explorer', 'logs explorer'], mount: async (appMountParams: ObservabilityLogExplorerAppMountParameters) => { const [coreStart, pluginsStart, ownPluginStart] = await core.getStartServices(); + const { renderObservabilityLogExplorer } = await import( + './applications/observability_log_explorer' + ); return renderObservabilityLogExplorer( coreStart, diff --git a/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx b/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx index b76a462eba25d..a58df53c7b1a7 100644 --- a/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx +++ b/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx @@ -19,7 +19,7 @@ export interface DatasetQualityRouteProps { export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => { const { services } = useKibanaContextForPlugin(); - const { observabilityShared, serverless, datasetQuality: DatasetQuality } = services; + const { serverless, datasetQuality: DatasetQuality } = services; const breadcrumb: EuiBreadcrumb[] = [ { text: datasetQualityAppTitle, @@ -29,13 +29,8 @@ export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => { useBreadcrumbs(breadcrumb, core.chrome, serverless); return ( - <> - - - - + + + ); }; diff --git a/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx index e17f92a46c23b..333d79bc6dd3c 100644 --- a/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx +++ b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx @@ -4,52 +4,106 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { CoreStart } from '@kbn/core/public'; -import React, { useMemo, useState } from 'react'; -import { BehaviorSubject } from 'rxjs'; +import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { + LogExplorerController, + LogExplorerPluginStart, +} from '@kbn/log-explorer-plugin/public'; +import { useActor } from '@xstate/react'; +import React, { useMemo } from 'react'; import { LogExplorerTopNavMenu } from '../../components/log_explorer_top_nav_menu'; import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template'; +import { createLogExplorerControllerWithCustomizations } from '../../log_explorer_customizations'; +import { + ObservabilityLogExplorerPageStateProvider, + useObservabilityLogExplorerPageStateContext, +} from '../../state_machines/observability_log_explorer/src'; +import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component'; +import { ObservabilityLogExplorerHistory } from '../../types'; import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs'; +import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; -import { ObservabilityLogExplorerAppMountParameters } from '../../types'; -import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component'; -import { createLogExplorerCustomizations } from '../../log_explorer_customizations'; -export interface ObservablityLogExplorerMainRouteProps { - appParams: ObservabilityLogExplorerAppMountParameters; - core: CoreStart; -} - -export const ObservablityLogExplorerMainRoute = ({ - appParams, - core, -}: ObservablityLogExplorerMainRouteProps) => { + +export const ObservabilityLogExplorerMainRoute = () => { const { services } = useKibanaContextForPlugin(); - const { logExplorer, observabilityShared, serverless } = services; - useBreadcrumbs(noBreadcrumbs, core.chrome, serverless); + const { logExplorer, serverless, chrome, notifications, appParams } = services; + const { history } = appParams; - const { history, setHeaderActionMenu, theme$ } = appParams; + useBreadcrumbs(noBreadcrumbs, chrome, serverless); - const [state$] = useState(() => new BehaviorSubject({})); + const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext(); - const customizations = useMemo(() => createLogExplorerCustomizations(), []); + const createLogExplorerController = useMemo( + () => createLogExplorerControllerWithCustomizations(logExplorer.createLogExplorerController), + [logExplorer.createLogExplorerController] + ); return ( - <> - + + + + + ); +}; + +const ConnectedContent = React.memo(() => { + const { + services: { + appParams: { history }, + logExplorer, + }, + } = useKibanaContextForPlugin(); + + const [state] = useActor(useObservabilityLogExplorerPageStateContext()); + + if (state.matches('initialized')) { + return ( + - - - ; + } +}); + +const InitializingContent = React.memo(() => ( + + } + title={ + + } + /> + +)); + +const InitializedContent = React.memo( + ({ + history, + logExplorer, + logExplorerController, + }: { + history: ObservabilityLogExplorerHistory; + logExplorer: LogExplorerPluginStart; + logExplorerController: LogExplorerController; + }) => { + return ( + + - - ); -}; + ); + } +); diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/controller_service.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/controller_service.ts new file mode 100644 index 0000000000000..05e2b1ba03922 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/controller_service.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public'; +import type { InvokeCreator } from 'xstate'; +import type { ObservabilityLogExplorerContext, ObservabilityLogExplorerEvent } from './types'; + +export const createController = + ({ + createLogExplorerController, + }: { + createLogExplorerController: CreateLogExplorerController; + }): InvokeCreator => + (context, event) => + (send) => { + createLogExplorerController({ + initialState: context.initialLogExplorerState, + }).then((controller) => { + send({ + type: 'CONTROLLER_CREATED', + controller, + }); + }); + }; + +export const subscribeToLogExplorerState: InvokeCreator< + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent +> = (context, event) => (send) => { + if (!('controller' in context)) { + throw new Error('Failed to subscribe to controller: no controller in context'); + } + + const { controller } = context; + + const subscription = controller.state$.subscribe({ + next: (state) => { + send({ type: 'LOG_EXPLORER_STATE_CHANGED', state }); + }, + }); + + controller.service.start(); + + return () => { + subscription.unsubscribe(); + controller.service.stop(); + }; +}; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/defaults.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/defaults.ts new file mode 100644 index 0000000000000..5e8c1eb5d3e2e --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/defaults.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. + */ + +import { CommonObservabilityLogExplorerContext } from './types'; + +export const DEFAULT_CONTEXT: CommonObservabilityLogExplorerContext = { + initialLogExplorerState: {}, +}; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/index.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/index.ts new file mode 100644 index 0000000000000..88b4fdbbd5d86 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/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 * from './provider'; +export * from './state_machine'; +export * from './types'; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/notifications.tsx b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/notifications.tsx new file mode 100644 index 0000000000000..050420de8875c --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/notifications.tsx @@ -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 { IToasts } from '@kbn/core-notifications-browser'; +import { mountReactNode } from '@kbn/core-mount-utils-browser-internal'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const createRequestFeedbackNotifier = (toasts: IToasts) => () => { + toasts.addInfo({ + title: i18n.translate('xpack.observabilityLogExplorer.feedbackToast.title', { + defaultMessage: 'Tell us what you think!', + }), + text: mountReactNode(<>), + iconType: 'editorComment', + }); +}; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/provider.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/provider.ts new file mode 100644 index 0000000000000..bfceafb1872df --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/provider.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getDevToolsOptions } from '@kbn/xstate-utils'; +import { useInterpret } from '@xstate/react'; +import createContainer from 'constate'; +import { + createObservabilityLogExplorerStateMachine, + ObservabilityLogExplorerStateMachineDependencies, +} from './state_machine'; + +export const useObservabilityLogExplorerPageState = ( + deps: ObservabilityLogExplorerStateMachineDependencies +) => { + const observabilityLogExplorerPageStateService = useInterpret( + () => createObservabilityLogExplorerStateMachine(deps), + { devTools: getDevToolsOptions() } + ); + + return observabilityLogExplorerPageStateService; +}; + +export const [ + ObservabilityLogExplorerPageStateProvider, + useObservabilityLogExplorerPageStateContext, +] = createContainer(useObservabilityLogExplorerPageState); diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/state_machine.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/state_machine.ts new file mode 100644 index 0000000000000..ecf77d069e08f --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/state_machine.ts @@ -0,0 +1,173 @@ +/* + * 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 { IToasts } from '@kbn/core-notifications-browser'; +import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public'; +import { actions, createMachine, InterpreterFrom } from 'xstate'; +import { TimefilterContract } from '@kbn/data-plugin/public'; +import { DEFAULT_CONTEXT } from './defaults'; +import { + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent, + ObservabilityLogExplorerTypeState, +} from './types'; +import { initializeFromUrl, updateUrlFromLogExplorerState } from './url_state_storage_service'; +import { createController, subscribeToLogExplorerState } from './controller_service'; +import { initializeFromTimeFilterService } from './time_filter_service'; + +export const createPureObservabilityLogExplorerStateMachine = ( + initialContext: ObservabilityLogExplorerContext +) => + /** @xstate-layout N4IgpgJg5mDOIC5QHkBGswCcBuBDVAlgDYEAuAngDID2UAogB4AOR1mWAdAK4B2BfpArhIAvSAGIA2gAYAuolBNqsMgWo8FIBogDMOjgFYAbAEZpAdgCcRgwBYAHAcsGANCHK7bHWztsGTRjomevbmRpYAvhFuaBg4+MRkVLSMLGyc-KrCBCL8UABimNQAtgAqBMVg+cSkWADKWNgEAMZg4gCSAHLtpe0AgpTtAFp0ACIA+vkASsgAsuO9s3ST7ZSldFPjdRsAau0AwnQy8kggSiqC6praCAC0ZvrhRua24Tp2jiZuHggATEb2Dj-aQ2PQmcEGSFRGLoRoJEgUGj0ZisdiYDiZQTZXI8ApFYoAVUwRA63V6A2GY0mM3mBKmlGOmnOqiupxutx0Rg49ksv2kBl+9hMQp0oV+30QBnsXgsAX8lhevx0lki0RAsThhARyWRaTRHGa7Fwglx+3UpCKRCIWHE+2QnVKM0olA2432UzofXWo0Zp2Zlw0bMQt0FHCMAN85jC9k5tl+cYlCFCJg4QRMxnM0lsZkcRmh6th8S1SSRqVRGQEQlEkG4PAA1jxqAB3HillHpTB1UjGtqUZAAcXGdAAGgAFPsezZ1Upe5b7AASfU6-bGvsUyhZgdANyVBg4JmzRl+UtFFks9nsiYh0n3tn59lM-2FNnzGqLiURKXb+sxVZyNbwEgIDbPV6m7WpxD7QcR3HZBJy2Gd1jdRdl1XOQmQ3ANrklUxDFCSxDwVX4TAIq9BXMIFnnTc9gXseMojVRsIDgTQ3zwYtP11ctMAwi41C3LRg3TFMnheN4Pn8RN7h0CjpDk6QDyFcxiOkX5VRhOJ2I-HUyw7Wtf2xSBeM3bCEAot4LyFHl42cEEpNsSxDHkkwlTMKwH2cV9Cy07UQO4jFK2xPJChKcpKmqIhak7RoWjAYysKDO57Bvayswc0EDxeMiuVscwdEFAUBRci9fi8zT4RLL9QPRAzRGC-EiSIeL+NMjkuXeNT3PDF5fFcdxEDjFNlNBSxwQvXKdDKzVtL8vTDTAY08jNHgLWoK0sGa1lt2DTkOGsJUHNGuSSJ8RMXhTEw8t8S61LkpwpvfXyqv82r-wgTaBPZOjvGSz49F5RxzEvfqEDMdMwzvBwAnMAwoxIh6fMqri9NesQIFrBtm1bZ6Oy7HsPta3wfukP7lQKoGr2fDhpEsTllNCAwdAUya1TYirON0n9AurdHAIIYCcbRPHagJxKjBp-cDCzA9LDuqxLEph9qdp55yMZhSDAYiIgA */ + createMachine< + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent, + ObservabilityLogExplorerTypeState + >( + { + context: initialContext, + predictableActionArguments: true, + id: 'ObservabilityLogExplorer', + initial: 'uninitialized', + states: { + uninitialized: { + always: 'initializingFromTimeFilterService', + }, + initializingFromTimeFilterService: { + invoke: { + src: 'initializeFromTimeFilterService', + }, + on: { + INITIALIZED_FROM_TIME_FILTER_SERVICE: { + target: 'initializingFromUrl', + actions: ['storeInitialTimeFilter'], + }, + }, + }, + initializingFromUrl: { + invoke: { + src: 'initializeFromUrl', + }, + on: { + INITIALIZED_FROM_URL: { + target: '#creatingController', + actions: ['storeInitialUrlState'], + }, + }, + }, + creatingController: { + id: 'creatingController', + invoke: { + src: 'createController', + }, + on: { + CONTROLLER_CREATED: { + target: 'initialized', + actions: ['storeController'], + }, + }, + }, + initialized: { + invoke: { + src: 'subscribeToLogExplorerState', + }, + + states: { + unknownLogExplorerState: { + on: { + LOG_EXPLORER_STATE_CHANGED: { + target: 'validLogExplorerState', + actions: ['storeLogExplorerState', 'updateUrlFromLogExplorerState'], + }, + }, + }, + + validLogExplorerState: { + on: { + LOG_EXPLORER_STATE_CHANGED: { + actions: ['storeLogExplorerState', 'updateUrlFromLogExplorerState'], + target: 'validLogExplorerState', + internal: true, + }, + }, + }, + }, + + initial: 'unknownLogExplorerState', + }, + }, + }, + { + actions: { + storeController: actions.assign((context, event) => { + return 'controller' in event && event.type === 'CONTROLLER_CREATED' + ? { controller: event.controller } + : {}; + }), + storeInitialTimeFilter: actions.assign((context, event) => { + return 'time' in event && + 'refreshInterval' in event && + event.type === 'INITIALIZED_FROM_TIME_FILTER_SERVICE' + ? { + initialLogExplorerState: { + ...('initialLogExplorerState' in context ? context.initialLogExplorerState : {}), + ...{ time: event.time, refreshInterval: event.refreshInterval }, + }, + } + : {}; + }), + storeInitialUrlState: actions.assign((context, event) => { + return 'stateFromUrl' in event && event.type === 'INITIALIZED_FROM_URL' + ? { + initialLogExplorerState: { + ...('initialLogExplorerState' in context ? context.initialLogExplorerState : {}), + ...event.stateFromUrl, + }, + } + : {}; + }), + storeLogExplorerState: actions.assign((context, event) => { + return 'state' in event && event.type === 'LOG_EXPLORER_STATE_CHANGED' + ? { logExplorerState: event.state } + : {}; + }), + }, + guards: {}, + } + ); + +export interface ObservabilityLogExplorerStateMachineDependencies { + createLogExplorerController: CreateLogExplorerController; + initialContext?: ObservabilityLogExplorerContext; + timeFilterService: TimefilterContract; + toasts: IToasts; + urlStateStorageContainer: IKbnUrlStateStorage; +} + +export const createObservabilityLogExplorerStateMachine = ({ + initialContext = DEFAULT_CONTEXT, + toasts, + urlStateStorageContainer, + createLogExplorerController, + timeFilterService, +}: ObservabilityLogExplorerStateMachineDependencies) => + createPureObservabilityLogExplorerStateMachine(initialContext).withConfig({ + actions: { + updateUrlFromLogExplorerState: updateUrlFromLogExplorerState({ urlStateStorageContainer }), + }, + services: { + createController: createController({ createLogExplorerController }), + initializeFromTimeFilterService: initializeFromTimeFilterService({ timeFilterService }), + initializeFromUrl: initializeFromUrl({ urlStateStorageContainer, toastsService: toasts }), + subscribeToLogExplorerState, + }, + }); + +export type ObservabilityLogExplorerService = InterpreterFrom< + typeof createObservabilityLogExplorerStateMachine +>; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/time_filter_service.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/time_filter_service.ts new file mode 100644 index 0000000000000..97b23a6b5198f --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/time_filter_service.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimefilterContract } from '@kbn/data-plugin/public'; +import { InvokeCreator } from 'xstate'; +import { ObservabilityLogExplorerContext, ObservabilityLogExplorerEvent } from './types'; + +export const initializeFromTimeFilterService = + ({ + timeFilterService, + }: { + timeFilterService: TimefilterContract; + }): InvokeCreator => + (_context, _event) => + (send) => { + const time = timeFilterService.getTime(); + const refreshInterval = timeFilterService.getRefreshInterval(); + + send({ + type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE', + time, + refreshInterval, + }); + }; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/types.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/types.ts new file mode 100644 index 0000000000000..be039af61708f --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/types.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryState } from '@kbn/data-plugin/common'; +import { + LogExplorerController, + LogExplorerPublicState, + LogExplorerPublicStateUpdate, +} from '@kbn/log-explorer-plugin/public'; + +export type ObservabilityLogExplorerContext = ObservabilityLogExplorerTypeState['context']; + +export interface CommonObservabilityLogExplorerContext { + initialLogExplorerState: LogExplorerPublicStateUpdate; +} + +export interface WithLogExplorerState { + logExplorerState: LogExplorerPublicState; +} + +export interface WithController { + controller: LogExplorerController; +} + +export type ObservabilityLogExplorerEvent = + | { + type: 'INITIALIZED_FROM_URL'; + stateFromUrl?: LogExplorerPublicStateUpdate; + } + | { + type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE'; + time: QueryState['time']; + refreshInterval: QueryState['refreshInterval']; + } + | { + type: 'CONTROLLER_CREATED'; + controller: LogExplorerController; + } + | { + type: 'LOG_EXPLORER_STATE_CHANGED'; + state: LogExplorerPublicState; + }; + +export type ObservabilityLogExplorerTypeState = + | { + value: + | 'uninitialized' + | 'initializingFromUrl' + | 'initializingFromTimeFilterService' + | 'creatingController'; + context: CommonObservabilityLogExplorerContext; + } + | { + value: 'initialized' | { initialized: 'unknownLogExplorerState' }; + context: CommonObservabilityLogExplorerContext & WithController; + } + | { + value: { initialized: 'validLogExplorerState' }; + context: CommonObservabilityLogExplorerContext & WithLogExplorerState & WithController; + }; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts new file mode 100644 index 0000000000000..353d55ea94324 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogExplorerPublicStateUpdate } from '@kbn/log-explorer-plugin/public'; +import * as rt from 'io-ts'; +import { deepCompactObject, urlSchemaV1 } from '../../../../common'; + +export const getStateFromUrlValue = ( + urlValue: urlSchemaV1.UrlSchema +): LogExplorerPublicStateUpdate => + deepCompactObject({ + chart: { + breakdownField: urlValue.breakdownField, + }, + controls: urlValue.controls, + datasetSelection: urlValue.datasetSelection, + filters: urlValue.filters, + grid: { + columns: urlValue.columns, + rows: { + rowHeight: urlValue.rowHeight, + rowsPerPage: urlValue.rowsPerPage, + }, + }, + query: urlValue.query, + refreshInterval: urlValue.refreshInterval, + time: urlValue.time, + }); + +export const getUrlValueFromState = (state: LogExplorerPublicStateUpdate): urlSchemaV1.UrlSchema => + deepCompactObject({ + breakdownField: state.chart?.breakdownField, + columns: state.grid?.columns, + controls: state.controls, + datasetSelection: state.datasetSelection, + filters: state.filters, + query: state.query, + refreshInterval: state.refreshInterval, + rowHeight: state.grid?.rows?.rowHeight, + rowsPerPage: state.grid?.rows?.rowsPerPage, + time: state.time, + v: 1, + }); + +const stateFromUrlSchemaRT = new rt.Type< + LogExplorerPublicStateUpdate, + urlSchemaV1.UrlSchema, + urlSchemaV1.UrlSchema +>( + 'stateFromUrlSchemaRT', + rt.never.is, + (urlSchema, context) => rt.success(getStateFromUrlValue(urlSchema)), + getUrlValueFromState +); + +export const stateFromUntrustedUrlRT = urlSchemaV1.urlSchemaRT.pipe(stateFromUrlSchemaRT); diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_state_storage_service.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_state_storage_service.ts new file mode 100644 index 0000000000000..2eef22e0e5269 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_state_storage_service.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IToasts } from '@kbn/core-notifications-browser'; +import { createPlainError, formatErrors } from '@kbn/io-ts-utils'; +import { IKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import * as Either from 'fp-ts/lib/Either'; +import * as rt from 'io-ts'; +import { InvokeCreator } from 'xstate'; +import { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY } from '../../../../common'; +import type { ObservabilityLogExplorerContext, ObservabilityLogExplorerEvent } from './types'; +import * as urlSchemaV1 from './url_schema_v1'; + +interface ObservabilityLogExplorerUrlStateDependencies { + toastsService: IToasts; + urlStateStorageContainer: IKbnUrlStateStorage; +} + +export const updateUrlFromLogExplorerState = + ({ urlStateStorageContainer }: { urlStateStorageContainer: IKbnUrlStateStorage }) => + (context: ObservabilityLogExplorerContext, event: ObservabilityLogExplorerEvent) => { + if (!('logExplorerState' in context)) { + return; + } + + // we want to write in the newest schema + const encodedUrlStateValues = urlSchemaV1.stateFromUntrustedUrlRT.encode( + context.logExplorerState + ); + + urlStateStorageContainer.set(OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, encodedUrlStateValues, { + replace: true, + }); + }; + +export const initializeFromUrl = + ({ + toastsService, + urlStateStorageContainer, + }: ObservabilityLogExplorerUrlStateDependencies): InvokeCreator< + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent + > => + (_context, _event) => + (send) => { + const urlStateValues = + urlStateStorageContainer.get(OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY) ?? undefined; + + // in the future we'll have to more schema versions to the union + const stateValuesE = rt + .union([rt.undefined, urlSchemaV1.stateFromUntrustedUrlRT]) + .decode(urlStateValues); + + if (Either.isLeft(stateValuesE)) { + withNotifyOnErrors(toastsService).onGetError( + createPlainError(formatErrors(stateValuesE.left)) + ); + send({ + type: 'INITIALIZED_FROM_URL', + stateFromUrl: undefined, + }); + } else { + send({ + type: 'INITIALIZED_FROM_URL', + stateFromUrl: stateValuesE.right, + }); + } + }; diff --git a/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx index 55b4b9359fd0b..9de71eb8069d7 100644 --- a/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx +++ b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx @@ -7,14 +7,14 @@ import { EuiBreadcrumb } from '@elastic/eui'; import type { ChromeStart } from '@kbn/core-chrome-browser'; -import type { ServerlessPluginStart } from '@kbn/serverless/public'; -import { useEffect } from 'react'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import { LOGS_APP_ID, - OBSERVABILITY_LOG_EXPLORER, + OBSERVABILITY_LOG_EXPLORER_APP_ID, OBSERVABILITY_OVERVIEW_APP_ID, } from '@kbn/deeplinks-observability'; +import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import { useEffect } from 'react'; import { logExplorerAppTitle, logsAppTitle, @@ -28,7 +28,7 @@ export const useBreadcrumbs = ( ) => { const observabilityLinkProps = useLinkProps({ app: OBSERVABILITY_OVERVIEW_APP_ID }); const logsLinkProps = useLinkProps({ app: LOGS_APP_ID }); - const logExplorerLinkProps = useLinkProps({ app: OBSERVABILITY_LOG_EXPLORER }); + const logExplorerLinkProps = useLinkProps({ app: OBSERVABILITY_LOG_EXPLORER_APP_ID }); useEffect(() => { setBreadcrumbs( diff --git a/x-pack/plugins/observability_log_explorer/public/utils/kbn_url_state_context.ts b/x-pack/plugins/observability_log_explorer/public/utils/kbn_url_state_context.ts new file mode 100644 index 0000000000000..167af63d0d9e6 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/utils/kbn_url_state_context.ts @@ -0,0 +1,35 @@ +/* + * 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 { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import createContainer from 'constate'; +import { useState } from 'react'; +import { useKibanaContextForPlugin } from './use_kibana'; + +const useKbnUrlStateStorageFromRouter = () => { + const { + services: { + appParams: { history }, + notifications: { toasts }, + uiSettings, + }, + } = useKibanaContextForPlugin(); + + const [urlStateStorage] = useState(() => + createKbnUrlStateStorage({ + history, + useHash: uiSettings.get('state:storeInSessionStorage'), + useHashQuery: false, + ...withNotifyOnErrors(toasts), + }) + ); + + return urlStateStorage; +}; + +export const [KbnUrlStateStorageFromRouterProvider, useKbnUrlStateStorageFromRouterContext] = + createContainer(useKbnUrlStateStorageFromRouter); diff --git a/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx b/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx index d8b2235586e55..bf18f5b6d1b2e 100644 --- a/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx +++ b/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx @@ -12,21 +12,29 @@ import { useKibana, } from '@kbn/kibana-react-plugin/public'; import { useMemo } from 'react'; -import { ObservabilityLogExplorerPluginStart, ObservabilityLogExplorerStartDeps } from '../types'; +import { + ObservabilityLogExplorerAppMountParameters, + ObservabilityLogExplorerPluginStart, + ObservabilityLogExplorerStartDeps, +} from '../types'; export type PluginKibanaContextValue = CoreStart & ObservabilityLogExplorerStartDeps & - ObservabilityLogExplorerPluginStart; + ObservabilityLogExplorerPluginStart & { + appParams: ObservabilityLogExplorerAppMountParameters; + }; export const createKibanaContextForPlugin = ( core: CoreStart, plugins: ObservabilityLogExplorerStartDeps, - pluginStart: ObservabilityLogExplorerPluginStart + pluginStart: ObservabilityLogExplorerPluginStart, + appParams: ObservabilityLogExplorerAppMountParameters ) => createKibanaReactContext({ ...core, ...plugins, ...pluginStart, + appParams, }); export const useKibanaContextForPlugin = @@ -35,11 +43,12 @@ export const useKibanaContextForPlugin = export const useKibanaContextForPluginProvider = ( core: CoreStart, plugins: ObservabilityLogExplorerStartDeps, - pluginStart: ObservabilityLogExplorerPluginStart + pluginStart: ObservabilityLogExplorerPluginStart, + appParams: ObservabilityLogExplorerAppMountParameters ) => { const { Provider } = useMemo( - () => createKibanaContextForPlugin(core, plugins, pluginStart), - [core, pluginStart, plugins] + () => createKibanaContextForPlugin(core, plugins, pluginStart, appParams), + [appParams, core, pluginStart, plugins] ); return Provider; diff --git a/x-pack/plugins/observability_log_explorer/tsconfig.json b/x-pack/plugins/observability_log_explorer/tsconfig.json index 24327c31c26a3..be61fb9926a8c 100644 --- a/x-pack/plugins/observability_log_explorer/tsconfig.json +++ b/x-pack/plugins/observability_log_explorer/tsconfig.json @@ -11,31 +11,31 @@ ".storybook/**/*.tsx" ], "kbn_references": [ + "@kbn/config-schema", "@kbn/core", - "@kbn/log-explorer-plugin", - "@kbn/i18n", - "@kbn/react-kibana-context-render", - "@kbn/shared-ux-router", - "@kbn/observability-shared-plugin", + "@kbn/core-chrome-browser", + "@kbn/core-mount-utils-browser-internal", + "@kbn/core-notifications-browser", "@kbn/data-plugin", + "@kbn/dataset-quality-plugin", + "@kbn/deeplinks-observability", + "@kbn/discover-plugin", + "@kbn/i18n", + "@kbn/i18n-react", + "@kbn/io-ts-utils", "@kbn/kibana-react-plugin", - "@kbn/serverless", - "@kbn/core-chrome-browser", - "@kbn/config-schema", "@kbn/kibana-utils-plugin", - "@kbn/discover-plugin", - "@kbn/es-query", + "@kbn/log-explorer-plugin", + "@kbn/logs-shared-plugin", + "@kbn/observability-shared-plugin", + "@kbn/react-kibana-context-render", "@kbn/react-kibana-mount", + "@kbn/serverless", "@kbn/share-plugin", - "@kbn/io-ts-utils", - "@kbn/deeplinks-observability", - "@kbn/core-notifications-browser", - "@kbn/core-mount-utils-browser-internal", - "@kbn/xstate-utils", + "@kbn/shared-ux-router", "@kbn/shared-ux-utility", "@kbn/ui-theme", - "@kbn/logs-shared-plugin", - "@kbn/dataset-quality-plugin" + "@kbn/xstate-utils" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/security_solution/common/endpoint/utils/package_v2.ts b/x-pack/plugins/security_solution/common/endpoint/utils/package_v2.ts index 926949bf9cd7f..fffa55ccbe514 100644 --- a/x-pack/plugins/security_solution/common/endpoint/utils/package_v2.ts +++ b/x-pack/plugins/security_solution/common/endpoint/utils/package_v2.ts @@ -7,7 +7,12 @@ import semverLte from 'semver/functions/lte'; -const MIN_ENDPOINT_PACKAGE_V2_VERSION = '8.12.0'; +function parseSemver(semver: string) { + return semver.includes('-') ? semver.substring(0, semver.indexOf('-')) : semver; +} + +const MIN_ENDPOINT_PACKAGE_V2_VERSION = '8.13.0'; export function isEndpointPackageV2(version: string) { - return semverLte(MIN_ENDPOINT_PACKAGE_V2_VERSION, version); + const parsedVersion = parseSemver(version); + return semverLte(MIN_ENDPOINT_PACKAGE_V2_VERSION, parsedVersion); } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 3e390eb29b42d..c9160c3cba1b9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -35,8 +35,6 @@ import { AllRules } from '../../components/rules_table'; import { RulesTableContextProvider } from '../../components/rules_table/rules_table/rules_table_context'; import { useInvalidateFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview_query'; import { HeaderPage } from '../../../../common/components/header_page'; -import { RulesPageTourComponent } from '../../components/rules_table/alternative_tour/tour'; -import { useIsEsqlRuleTypeEnabled } from '../../../rule_creation/hooks'; const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); @@ -72,8 +70,6 @@ const RulesPageComponent: React.FC = () => { } = useListsConfig(); const loading = userInfoLoading || listsConfigLoading; - const isEsqlRuleTypeEnabled = useIsEsqlRuleTypeEnabled(); - if ( redirectToDetections( isSignalIndexExists, @@ -89,18 +85,6 @@ const RulesPageComponent: React.FC = () => { return null; } - const addNewRuleButton = ( - - {i18n.ADD_NEW_RULE} - - ); - return ( <> @@ -155,11 +139,15 @@ const RulesPageComponent: React.FC = () => {
- {isEsqlRuleTypeEnabled ? ( - {addNewRuleButton} - ) : ( - addNewRuleButton - )} + + {i18n.ADD_NEW_RULE} +
diff --git a/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.tsx new file mode 100644 index 0000000000000..8d719485973e4 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents.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, { useEffect, useState } from 'react'; + +import { + DocumentList, + DocumentsOverview, + INDEX_DOCUMENTS_META_DEFAULT, +} from '@kbn/search-index-documents'; + +import { i18n } from '@kbn/i18n'; +import { useIndexDocumentSearch } from '../../hooks/api/use_index_documents'; +import { useIndexMappings } from '../../hooks/api/use_index_mappings'; + +const DEFAULT_PAGINATION = { + pageIndex: INDEX_DOCUMENTS_META_DEFAULT.pageIndex, + pageSize: INDEX_DOCUMENTS_META_DEFAULT.pageSize, + totalItemCount: INDEX_DOCUMENTS_META_DEFAULT.totalItemCount, +}; + +interface IndexDocumentsProps { + indexName: string; +} + +export const IndexDocuments: React.FC = ({ indexName }) => { + const [pagination, setPagination] = useState(DEFAULT_PAGINATION); + const [searchQuery, setSearchQuery] = useState(''); + const searchQueryCallback = (query: string) => { + setSearchQuery(query); + }; + const { results: indexDocuments, meta: documentsMeta } = useIndexDocumentSearch( + indexName, + pagination, + searchQuery + ); + + const { data: mappingData } = useIndexMappings(indexName); + + const docs = indexDocuments?.data ?? []; + + useEffect(() => { + setSearchQuery(''); + setPagination(DEFAULT_PAGINATION); + }, [indexName]); + return ( + + {docs.length === 0 && + i18n.translate('xpack.serverlessSearch.indexManagementTab.documents.noMappings', { + defaultMessage: 'No documents found for index', + })} + {docs?.length > 0 && ( + setPagination({ ...pagination, pageIndex })} + setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })} + /> + )} + + } + /> + ); +}; diff --git a/x-pack/plugins/serverless_search/public/application/components/index_documents/documents_tab.tsx b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents_tab.tsx new file mode 100644 index 0000000000000..c71089b5f3b83 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/components/index_documents/documents_tab.tsx @@ -0,0 +1,43 @@ +/* + * 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 { IndexDetailsTab } from '@kbn/index-management-plugin/common/constants'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { ServerlessSearchPluginStartDependencies } from '../../../types'; +import { IndexDocuments } from './documents'; + +export const createIndexOverviewContent = ( + core: CoreStart, + services: ServerlessSearchPluginStartDependencies +): IndexDetailsTab => { + return { + id: 'documents', + name: ( + + ), + order: 11, + renderTabContent: ({ index }) => { + const queryClient = new QueryClient(); + return ( + + + + + + + ); + }, + }; +}; diff --git a/x-pack/plugins/serverless_search/public/application/hooks/api/use_index_documents.tsx b/x-pack/plugins/serverless_search/public/application/hooks/api/use_index_documents.tsx new file mode 100644 index 0000000000000..7c95376b295d8 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/hooks/api/use_index_documents.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Pagination } from '@elastic/eui'; +import { SearchHit } from '@kbn/es-types'; +import { pageToPagination, Paginate } from '@kbn/search-index-documents'; +import { useQuery } from '@tanstack/react-query'; +import { useKibanaServices } from '../use_kibana'; + +interface IndexDocuments { + meta: Pagination; + results: Paginate; +} +const DEFAULT_PAGINATION = { + from: 0, + has_more_hits_than_total: false, + size: 10, + total: 0, +}; +export const useIndexDocumentSearch = ( + indexName: string, + pagination: Omit, + searchQuery?: string +) => { + const { http } = useKibanaServices(); + const response = useQuery({ + queryKey: ['fetchIndexDocuments', pagination, searchQuery], + queryFn: async () => + http.post(`/internal/serverless_search/indices/${indexName}/search`, { + body: JSON.stringify({ + searchQuery, + }), + query: { + page: pagination.pageIndex, + size: pagination.pageSize, + }, + }), + }); + return { + results: response?.data?.results, + meta: pageToPagination(response?.data?.results?._meta?.page ?? DEFAULT_PAGINATION), + }; +}; diff --git a/x-pack/plugins/serverless_search/public/application/hooks/api/use_index_mappings.tsx b/x-pack/plugins/serverless_search/public/application/hooks/api/use_index_mappings.tsx new file mode 100644 index 0000000000000..28ee739bc6700 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/hooks/api/use_index_mappings.tsx @@ -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 { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; +import { useQuery } from '@tanstack/react-query'; +import { useKibanaServices } from '../use_kibana'; + +export const useIndexMappings = (indexName: string) => { + const { http } = useKibanaServices(); + return useQuery({ + queryKey: ['fetchIndexMappings'], + queryFn: async () => + http.fetch( + `/internal/serverless_search/mappings/${indexName}` + ), + }); +}; diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts index 4203925650385..f82af8390c9fc 100644 --- a/x-pack/plugins/serverless_search/public/plugin.ts +++ b/x-pack/plugins/serverless_search/public/plugin.ts @@ -24,6 +24,7 @@ import { ServerlessSearchPluginStart, ServerlessSearchPluginStartDependencies, } from './types'; +import { createIndexOverviewContent } from './application/components/index_documents/documents_tab'; export class ServerlessSearchPlugin implements @@ -80,14 +81,14 @@ export class ServerlessSearchPlugin return await renderApp(element, coreStart, { history, ...services }); }, }); - return {}; } public start( core: CoreStart, - { serverless, management, cloud, indexManagement }: ServerlessSearchPluginStartDependencies + services: ServerlessSearchPluginStartDependencies ): ServerlessSearchPluginStart { + const { serverless, management, cloud, indexManagement } = services; serverless.setProjectHome('/app/elasticsearch'); serverless.setSideNavComponent(createComponent(core, { serverless, cloud })); management.setIsSidebarEnabled(false); @@ -96,6 +97,10 @@ export class ServerlessSearchPlugin hideLinksTo: [appIds.MAINTENANCE_WINDOWS], }); indexManagement?.extensionsService.setIndexMappingsContent(createIndexMappingsContent(core)); + indexManagement?.extensionsService.addIndexDetailsTab( + createIndexOverviewContent(core, services) + ); + return {}; } diff --git a/x-pack/plugins/serverless_search/server/plugin.ts b/x-pack/plugins/serverless_search/server/plugin.ts index 6e6a90735ee23..e533ae6023380 100644 --- a/x-pack/plugins/serverless_search/server/plugin.ts +++ b/x-pack/plugins/serverless_search/server/plugin.ts @@ -28,6 +28,7 @@ import type { } from './types'; import { registerConnectorsRoutes } from './routes/connectors_routes'; import { registerTelemetryUsageCollector } from './collectors/connectors/telemetry'; +import { registerMappingRoutes } from './routes/mapping_routes'; export interface RouteDependencies { http: CoreSetup['http']; @@ -93,6 +94,7 @@ export class ServerlessSearchPlugin registerApiKeyRoutes(dependencies); registerConnectorsRoutes(dependencies); registerIndicesRoutes(dependencies); + registerMappingRoutes(dependencies); }); if (usageCollection) { diff --git a/x-pack/plugins/serverless_search/server/routes/indices_routes.ts b/x-pack/plugins/serverless_search/server/routes/indices_routes.ts index 9cac99f30ab31..7f165f13218f1 100644 --- a/x-pack/plugins/serverless_search/server/routes/indices_routes.ts +++ b/x-pack/plugins/serverless_search/server/routes/indices_routes.ts @@ -8,6 +8,8 @@ import { IndicesIndexState } from '@elastic/elasticsearch/lib/api/types'; import { schema } from '@kbn/config-schema'; +import { fetchSearchResults } from '@kbn/search-index-documents/lib'; +import { DEFAULT_DOCS_PER_PAGE } from '@kbn/search-index-documents/types'; import { fetchIndices } from '../lib/indices/fetch_indices'; import { RouteDependencies } from '../plugin'; @@ -72,6 +74,45 @@ export const registerIndicesRoutes = ({ router, security }: RouteDependencies) = }); } ); + + router.post( + { + path: '/internal/serverless_search/indices/{index_name}/search', + validate: { + body: schema.object({ + searchQuery: schema.string({ + defaultValue: '', + }), + }), + params: schema.object({ + index_name: schema.string(), + }), + query: schema.object({ + page: schema.number({ defaultValue: 0, min: 0 }), + size: schema.number({ + defaultValue: DEFAULT_DOCS_PER_PAGE, + min: 0, + }), + }), + }, + }, + async (context, request, response) => { + const client = (await context.core).elasticsearch.client.asCurrentUser; + const indexName = decodeURIComponent(request.params.index_name); + const searchQuery = request.body.searchQuery; + const { page = 0, size = DEFAULT_DOCS_PER_PAGE } = request.query; + const from = page * size; + + const searchResults = await fetchSearchResults(client, indexName, searchQuery, from, size); + + return response.ok({ + body: { + results: searchResults, + }, + headers: { 'content-type': 'application/json' }, + }); + } + ); }; function isHidden(index?: IndicesIndexState): boolean { diff --git a/x-pack/plugins/serverless_search/server/routes/mapping_routes.ts b/x-pack/plugins/serverless_search/server/routes/mapping_routes.ts new file mode 100644 index 0000000000000..bb6e22a1bd8fe --- /dev/null +++ b/x-pack/plugins/serverless_search/server/routes/mapping_routes.ts @@ -0,0 +1,33 @@ +/* + * 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 { RouteDependencies } from '../plugin'; + +export const registerMappingRoutes = ({ router }: RouteDependencies) => { + router.get( + { + path: '/internal/serverless_search/mappings/{index_name}', + validate: { + params: schema.object({ + index_name: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + const mapping = await client.asCurrentUser.indices.getMapping({ + expand_wildcards: ['open'], + index: request.params.index_name, + }); + return response.ok({ + body: mapping[request.params.index_name], + headers: { 'content-type': 'application/json' }, + }); + } + ); +}; diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json index 6e00a0f988c2d..e8cad3756b974 100644 --- a/x-pack/plugins/serverless_search/tsconfig.json +++ b/x-pack/plugins/serverless_search/tsconfig.json @@ -30,6 +30,7 @@ "@kbn/management-cards-navigation", "@kbn/core-elasticsearch-server", "@kbn/search-api-panels", + "@kbn/search-index-documents", "@kbn/serverless-search-settings", "@kbn/core-lifecycle-browser", "@kbn/react-kibana-context-theme", @@ -39,5 +40,6 @@ "@kbn/kibana-utils-plugin", "@kbn/index-management-plugin", "@kbn/usage-collection-plugin", + "@kbn/es-types", ] } diff --git a/x-pack/plugins/stack_connectors/common/experimental_features.ts b/x-pack/plugins/stack_connectors/common/experimental_features.ts index 4ac02dd9f06db..5e114524e0b19 100644 --- a/x-pack/plugins/stack_connectors/common/experimental_features.ts +++ b/x-pack/plugins/stack_connectors/common/experimental_features.ts @@ -16,7 +16,7 @@ export const allowedExperimentalValues = Object.freeze({ sentinelOneConnectorOn: false, }); -type ExperimentalConfigKeys = Array; +export type ExperimentalConfigKeys = Array; type Mutable = { -readonly [P in keyof T]: T[P] }; const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly; 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 777d78acb192f..a7098dae6a150 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2865,6 +2865,12 @@ "description": "Total number of services utilizing the opentelemetry/swift agent within the last day" } }, + "opentelemetry/android": { + "type": "long", + "_meta": { + "description": "Total number of services utilizing the opentelemetry/android agent within the last day" + } + }, "opentelemetry/webjs": { "type": "long", "_meta": { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 4a5c665911495..88f3cce226a70 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -10,6 +10,8 @@ import getPort from 'get-port'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test'; import { getAllExternalServiceSimulatorPaths } from '@kbn/actions-simulators-plugin/server/plugin'; +import { ExperimentalConfigKeys } from '@kbn/stack-connectors-plugin/common/experimental_features'; +import { SENTINELONE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/sentinelone/constants'; import { services } from './services'; import { getTlsWebhookServerUrls } from './lib/get_tls_webhook_servers'; @@ -29,6 +31,7 @@ interface CreateTestConfigOptions { useDedicatedTaskRunner: boolean; enableFooterInEmail?: boolean; maxScheduledPerMinute?: number; + experimentalFeatures?: ExperimentalConfigKeys; } // test.not-enabled is specifically not enabled @@ -48,6 +51,7 @@ const enabledActionTypes = [ '.resilient', '.gen-ai', '.d3security', + SENTINELONE_CONNECTOR_ID, '.slack', '.slack_api', '.tines', @@ -85,6 +89,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) useDedicatedTaskRunner, enableFooterInEmail = true, maxScheduledPerMinute, + experimentalFeatures = [], } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -344,6 +349,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.task_manager.allow_reading_invalid_state=false', '--xpack.task_manager.requeue_invalid_tasks.enabled=true', '--xpack.actions.queued.max=500', + `--xpack.stack_connectors.enableExperimental=${JSON.stringify(experimentalFeatures)}`, ], }, }; diff --git a/x-pack/test/alerting_api_integration/common/lib/log_supertest_errors.ts b/x-pack/test/alerting_api_integration/common/lib/log_supertest_errors.ts new file mode 100644 index 0000000000000..d8b0034606293 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/log_supertest_errors.ts @@ -0,0 +1,66 @@ +/* + * 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 SuperTest from 'supertest'; +import { ToolingLog } from '@kbn/tooling-log'; + +export interface LogErrorDetailsInterface { + (this: SuperTest.Test, err: Error & { response?: any }): SuperTest.Test; + ignoreCodes: ( + codes: number[] + ) => (this: SuperTest.Test, err: Error & { response?: SuperTest.Response }) => SuperTest.Test; +} + +/** + * Creates a logger that can be used with `supertest` to log details around errors + * + * @param log + * + * @example + * const errorLogger = createSupertestErrorLogger(log); + * supertestWithoutAuth + * .post(`some/url`) + * .on('error', errorLogger) //<< Add logger to `error` event + * .send({}) + */ +export const createSupertestErrorLogger = (log: ToolingLog): LogErrorDetailsInterface => { + /** + * Utility for use with `supertest` that logs errors with details returned by the API + * @param err + */ + const logErrorDetails: LogErrorDetailsInterface = function (err) { + if (err.response && (err.response.body || err.response.text)) { + let outputData = + 'RESPONSE:\n' + err.response.body + ? JSON.stringify(err.response.body, null, 2) + : err.response.text; + + if (err.response.request) { + const { url = '', method = '', _data = '' } = err.response.request; + + outputData += `\nREQUEST: + ${method} ${url} + ${JSON.stringify(_data, null, 2)} + `; + } + + log.error(outputData); + } + + return this ?? err; + }; + logErrorDetails.ignoreCodes = (codes) => { + return function (err) { + if (err.response && err.response.status && !codes.includes(err.response.status)) { + return logErrorDetails.call(this, err); + } + return this; + }; + }; + + return logErrorDetails; +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts index f999da061b90b..8f52f7c40d005 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts @@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', { publicBaseUrl: true, testFiles: [require.resolve('./tests')], useDedicatedTaskRunner: true, + experimentalFeatures: ['sentinelOneConnectorOn'], }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts index 7aed7501bc012..56e99c4c2d5e4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts @@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', { publicBaseUrl: true, testFiles: [require.resolve('./tests')], useDedicatedTaskRunner: false, + experimentalFeatures: ['sentinelOneConnectorOn'], }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts index 589f31656736c..599f370122358 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts @@ -133,7 +133,8 @@ export default function ({ getService }: FtrProviderContext) { connectorId, outcome: 'failure', message: `action execution failure: test.system-action-kibana-privileges:${connectorId}: ${name}`, - errorMessage: 'Unauthorized to execute actions', + errorMessage: + 'Unauthorized to execute a "test.system-action-kibana-privileges" action', startDate, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts index e6336b7092087..77dce4971e9ed 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts @@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', { publicBaseUrl: true, testFiles: [require.resolve('.')], useDedicatedTaskRunner: true, + experimentalFeatures: ['sentinelOneConnectorOn'], }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts new file mode 100644 index 0000000000000..87ec7de447945 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts @@ -0,0 +1,240 @@ +/* + * 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 { + SENTINELONE_CONNECTOR_ID, + SUB_ACTION, +} from '@kbn/stack-connectors-plugin/common/sentinelone/constants'; +import { FeaturesPrivileges, Role } from '@kbn/security-plugin/common'; +import SuperTest from 'supertest'; +import expect from '@kbn/expect'; +import { getUrlPrefix } from '../../../../../common/lib'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { createSupertestErrorLogger } from '../../../../../common/lib/log_supertest_errors'; + +// eslint-disable-next-line import/no-default-export +export default function createSentinelOneTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const securityService = getService('security'); + const log = getService('log'); + const logErrorDetails = createSupertestErrorLogger(log); + + describe('SentinelOne', () => { + describe('sub-actions authz', () => { + interface CreatedUser { + username: string; + password: string; + deleteUser: () => Promise; + } + + // SentinelOne supported sub-actions + const s1SubActions = [ + SUB_ACTION.KILL_PROCESS, + SUB_ACTION.GET_AGENTS, + SUB_ACTION.ISOLATE_HOST, + SUB_ACTION.RELEASE_HOST, + SUB_ACTION.GET_REMOTE_SCRIPT_STATUS, + SUB_ACTION.GET_REMOTE_SCRIPT_RESULTS, + ]; + + let connectorId: string; + + const createUser = async ({ + username, + password = 'changeme', + kibanaFeatures = { actions: ['all'] }, + }: { + username: string; + password?: string; + kibanaFeatures?: FeaturesPrivileges; + }): Promise => { + const role: Role = { + name: username, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + // Important: Saved Objects Managemnt should be set to `all` to ensure that authz + // is not defaulted to the check done against SO's for SentinelOne + savedObjectsManagement: ['all'], + ...kibanaFeatures, + }, + spaces: ['*'], + }, + ], + }; + + await securityService.role.create(role.name, { + kibana: role.kibana, + elasticsearch: role.elasticsearch, + }); + + await securityService.user.create(username, { + password: 'changeme', + full_name: role.name, + roles: [role.name], + }); + + return { + username, + password, + deleteUser: async () => { + await securityService.user.delete(role.name); + await securityService.role.delete(role.name); + }, + }; + }; + + before(async () => { + const response = await supertest + .post(`${getUrlPrefix('default')}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .on('error', logErrorDetails) + .send({ + name: 'My sub connector', + connector_type_id: SENTINELONE_CONNECTOR_ID, + config: { url: 'https://some.non.existent.com' }, + secrets: { token: 'abc-123' }, + }) + .expect(200); + + connectorId = response.body.id; + }); + + after(async () => { + if (connectorId) { + await supertest + .delete(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}`) + .set('kbn-xsrf', 'true') + .send() + .expect(({ ok, status }) => { + // Should cover all success codes (ex. 204 (no content), 200, etc...) + if (!ok) { + throw new Error( + `Expected delete to return a status code in the 200, but got ${status}` + ); + } + }); + + connectorId = ''; + } + }); + + const executeSubAction = async ({ + subAction, + subActionParams, + expectedHttpCode = 200, + username = 'elastic', + password = 'changeme', + errorLogger = logErrorDetails, + }: { + supertest: SuperTest.SuperTest; + subAction: string; + subActionParams: Record; + expectedHttpCode?: number; + username?: string; + password?: string; + errorLogger?: (err: any) => void; + }) => { + return supertestWithoutAuth + .post(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}/_execute`) + .set('kbn-xsrf', 'foo') + .on('error', errorLogger) + .auth(username, password) + .send({ + params: { + subAction, + subActionParams, + }, + }) + .expect(expectedHttpCode); + }; + + describe('and user has NO privileges', () => { + let user: CreatedUser; + + before(async () => { + user = await createUser({ + username: 'read_access_user', + kibanaFeatures: { actions: ['read'] }, + }); + }); + + after(async () => { + if (user) { + await user.deleteUser(); + } + }); + + for (const s1SubAction of s1SubActions) { + it(`should deny execute of ${s1SubAction}`, async () => { + const execRes = await executeSubAction({ + supertest: supertestWithoutAuth, + subAction: s1SubAction, + subActionParams: {}, + username: user.username, + password: user.password, + expectedHttpCode: 403, + errorLogger: logErrorDetails.ignoreCodes([403]), + }); + + expect(execRes.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to execute a ".sentinelone" action', + }); + }); + } + }); + + describe('and user has proper privileges', () => { + let user: CreatedUser; + + before(async () => { + user = await createUser({ + username: 'all_access_user', + }); + }); + + after(async () => { + if (user) { + await user.deleteUser(); + // @ts-expect-error + user = undefined; + } + }); + + for (const s1SubAction of s1SubActions) { + it(`should allow execute of ${s1SubAction}`, async () => { + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + body: { status, message, connector_id }, + } = await executeSubAction({ + supertest: supertestWithoutAuth, + subAction: s1SubAction, + subActionParams: {}, + username: user.username, + password: user.password, + }); + + expect({ status, message, connector_id }).to.eql({ + status: 'error', + message: 'an error occurred while running the action', + connector_id: connectorId, + }); + }); + } + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts index 8433570e08241..d12bc59aca4df 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts @@ -41,12 +41,13 @@ export default function ({ getService }: FtrProviderContext) { const { user, space } = scenario; describe(scenario.id, () => { it('should handle execute request appropriately', async () => { + const connectorTypeId = 'test.index-record'; const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - connector_type_id: 'test.index-record', + connector_type_id: connectorTypeId, config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -78,7 +79,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: 'Unauthorized to execute actions', + message: `Unauthorized to execute a "${connectorTypeId}" action`, }); break; case 'global_read at space1': @@ -161,7 +162,7 @@ export default function ({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(403); + expect(response.statusCode).to.eql(403, response.text); expect(response.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -184,12 +185,13 @@ export default function ({ getService }: FtrProviderContext) { }); it('should handle execute request appropriately after action is updated', async () => { + const connectorTypeId = 'test.index-record'; const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - connector_type_id: 'test.index-record', + connector_type_id: connectorTypeId, config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -235,7 +237,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: 'Unauthorized to execute actions', + message: `Unauthorized to execute a "${connectorTypeId}" action`, }); break; case 'global_read at space1': @@ -286,7 +288,7 @@ export default function ({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(403); + expect(response.statusCode).to.eql(403, response.text); expect(response.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -340,12 +342,13 @@ export default function ({ getService }: FtrProviderContext) { }); it('should handle execute request appropriately after changing config properties', async () => { + const connectorTypeId = '.email'; const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'test email action', - connector_type_id: '.email', + connector_type_id: connectorTypeId, config: { from: 'email-from-1@example.com', // this host is specifically added to allowedHosts in: @@ -397,7 +400,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: 'Unauthorized to execute actions', + message: `Unauthorized to execute a "${connectorTypeId}" action`, }); break; case 'global_read at space1': @@ -416,12 +419,13 @@ export default function ({ getService }: FtrProviderContext) { let indexedRecord: any; let searchResult: any; const reference = `actions-execute-3:${user.username}`; + const connectorTypeId = 'test.authorization'; const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - connector_type_id: 'test.authorization', + connector_type_id: connectorTypeId, }) .expect(200); objectRemover.add(space.id, createdAction.id, 'action', 'actions'); @@ -448,7 +452,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: 'Unauthorized to execute actions', + message: `Unauthorized to execute a "${connectorTypeId}" action`, }); break; case 'global_read at space1': @@ -528,7 +532,7 @@ export default function ({ getService }: FtrProviderContext) { case 'global_read at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(403); + expect(response.statusCode).to.eql(403, response.text); expect(response.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -542,7 +546,7 @@ export default function ({ getService }: FtrProviderContext) { */ case 'superuser at space1': case 'system_actions at space1': - expect(response.statusCode).to.eql(200); + expect(response.statusCode).to.eql(200, response.text); await validateSystemEventLog({ spaceId: space.id, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts index e9fc2cbeb7c00..dc1876d6e784b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts @@ -32,6 +32,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide loadTestFile(require.resolve('./connector_types/es_index_preconfigured')); loadTestFile(require.resolve('./connector_types/opsgenie')); loadTestFile(require.resolve('./connector_types/pagerduty')); + loadTestFile(require.resolve('./connector_types/sentinelone')); loadTestFile(require.resolve('./connector_types/server_log')); loadTestFile(require.resolve('./connector_types/slack_webhook')); loadTestFile(require.resolve('./connector_types/slack_api')); diff --git a/x-pack/test/functional/apps/discover/saved_queries.ts b/x-pack/test/functional/apps/discover/saved_queries.ts index 66ceb0dd974df..2795ce3beafd6 100644 --- a/x-pack/test/functional/apps/discover/saved_queries.ts +++ b/x-pack/test/functional/apps/discover/saved_queries.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -12,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const spaces = getService('spaces'); + const toasts = getService('toasts'); const PageObjects = getPageObjects([ 'common', 'discover', @@ -71,6 +73,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); await savedQueryManagementComponent.savedQueryMissingOrFail(savedQueryName); }); + + it('updates a saved query', async () => { + // Navigate to Discover & create a saved query + await PageObjects.common.navigateToApp('discover'); + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.saveNewQuery(savedQueryName, '', true, false); + await savedQueryManagementComponent.savedQueryExistOrFail(savedQueryName); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); + + // Navigate to Discover & create a saved query + await queryBar.setQuery('response:404'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery('', true, false); + + // Expect to see a success toast + const successToast = await toasts.getToastElement(1); + const successText = await successToast.getVisibleText(); + expect(successText).to.equal(`Your query "${savedQueryName}" was saved`); + + await PageObjects.common.navigateToApp('discover'); + await savedQueryManagementComponent.deleteSavedQuery(savedQueryName); + }); }); }); } diff --git a/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts index b23fd6813c0f6..5da73c3e5ecb5 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts @@ -5,7 +5,6 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; import { FtrProviderContext } from './config'; const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'message']; @@ -39,10 +38,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should restore the table columns from the URL state if exists', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ - columns: ['service.name', 'host.name', 'message', 'data_stream.namespace'], - }), + pageState: { + columns: [ + { field: 'service.name' }, + { field: 'host.name' }, + { field: 'message' }, + { field: 'data_stream.namespace' }, + ], }, }); diff --git a/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts index 9fc2cca312551..583313ec8cb9a 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts @@ -5,16 +5,30 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; +import { decodeOrThrow, indexPatternRt } from '@kbn/io-ts-utils'; +import { DatasetSelectionPlain } from '@kbn/log-explorer-plugin/common'; import { FtrProviderContext } from './config'; +const azureActivityDatasetSelection: DatasetSelectionPlain = { + selection: { + dataset: { + name: decodeOrThrow(indexPatternRt)('logs-azure.activitylogs-*'), + title: 'activitylogs', + }, + name: 'azure', + title: 'Azure Logs', + version: '1.5.23', + }, + selectionType: 'single', +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'observabilityLogExplorer']); describe('DatasetSelection initialization and update', () => { - describe('when the "index" query param does not exist', () => { + describe('when no dataset selection is given', () => { it('should initialize the "All logs" selection', async () => { await PageObjects.observabilityLogExplorer.navigateTo(); const datasetSelectionTitle = @@ -24,13 +38,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('when the "index" query param exists', () => { - it('should decode and restore the selection from a valid encoded index', async () => { - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; + describe('when a dataset selection is given', () => { + it('should restore the selection from a valid encoded index', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); @@ -41,10 +53,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should fallback to the "All logs" selection and notify the user of an invalid encoded index', async () => { - const invalidEncodedIndex = 'invalid-encoded-index'; - await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: invalidEncodedIndex }), + await PageObjects.observabilityLogExplorer.navigateToWithUncheckedState({ + pageState: { + v: 1, + datasetSelection: { + selectionType: 'invalid', + }, }, }); @@ -63,12 +77,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(allDatasetSelectionTitle).to.be('All logs'); - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), - controlPanels: rison.encode({}), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); const azureDatasetSelectionTitle = diff --git a/x-pack/test/functional/apps/observability_log_explorer/flyout.ts b/x-pack/test/functional/apps/observability_log_explorer/flyout.ts index b2b71c817255a..11c161b5f4f8d 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/flyout.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/flyout.ts @@ -49,8 +49,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts b/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts index 3b71935cd6fbc..11dc71409b463 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts @@ -68,8 +68,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -125,8 +130,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -187,8 +197,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -252,8 +267,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test/functional/page_objects/observability_log_explorer.ts b/x-pack/test/functional/page_objects/observability_log_explorer.ts index 0b3266465c32e..a992aeefd0541 100644 --- a/x-pack/test/functional/page_objects/observability_log_explorer.ts +++ b/x-pack/test/functional/page_objects/observability_log_explorer.ts @@ -5,6 +5,10 @@ * 2.0. */ import expect from '@kbn/expect'; +import { + OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, + urlSchemaV1, +} from '@kbn/observability-log-explorer-plugin/common'; import rison from '@kbn/rison'; import querystring from 'querystring'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; @@ -101,8 +105,14 @@ const packages: IntegrationPackage[] = [ const initialPackages = packages.slice(0, 3); const additionalPackages = packages.slice(3); -const FROM = '2023-08-03T10:24:14.035Z'; -const TO = '2023-08-03T10:24:14.091Z'; +const defaultPageState: urlSchemaV1.UrlSchema = { + v: 1, + time: { + from: '2023-08-03T10:24:14.035Z', + to: '2023-08-03T10:24:14.091Z', + mode: 'absolute', + }, +}; export function ObservabilityLogExplorerPageObject({ getPageObjects, @@ -117,15 +127,6 @@ export function ObservabilityLogExplorerPageObject({ const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); - type NavigateToAppOptions = Omit< - Parameters[1], - 'search' - > & { - search?: Record; - from?: string; - to?: string; - }; - return { uninstallPackage: ({ name, version }: IntegrationPackage) => { return supertest.delete(`/api/fleet/epm/packages/${name}/${version}`).set('kbn-xsrf', 'xxxx'); @@ -208,19 +209,45 @@ export function ObservabilityLogExplorerPageObject({ }; }, - async navigateTo(options: NavigateToAppOptions = {}) { - const { search = {}, from = FROM, to = TO, ...extraOptions } = options; - const composedSearch = querystring.stringify({ - ...search, - _g: rison.encode({ - time: { from, to }, - }), + async navigateTo({ + pageState, + }: { + pageState?: urlSchemaV1.UrlSchema; + } = {}) { + const queryStringParams = querystring.stringify({ + [OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY]: rison.encode( + urlSchemaV1.urlSchemaRT.encode({ + ...defaultPageState, + ...pageState, + }) + ), }); - return await PageObjects.common.navigateToApp('observabilityLogExplorer', { - search: composedSearch, - ...extraOptions, + return await PageObjects.common.navigateToUrlWithBrowserHistory( + 'observabilityLogExplorer', + '/', + queryStringParams + ); + }, + + async navigateToWithUncheckedState({ + pageState: uncheckedPageState, + }: { + pageState?: {}; + } = {}) { + const queryStringParams = querystring.stringify({ + [OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY]: rison.encode({ + ...uncheckedPageState, + }), }); + + log.info('queryStringParams'); + + return await PageObjects.common.navigateToUrlWithBrowserHistory( + 'observabilityLogExplorer', + '/', + queryStringParams + ); }, getDatasetSelector() { @@ -357,9 +384,7 @@ export function ObservabilityLogExplorerPageObject({ async assertRestoreFailureToastExist() { const successToast = await toasts.getToastElement(1); - expect(await successToast.getVisibleText()).to.contain( - "We couldn't restore your datasets selection" - ); + expect(await successToast.getVisibleText()).to.contain('Error restoring state from URL'); }, assertLoadingSkeletonExists() { diff --git a/x-pack/test/security_solution_cypress/cypress/README.md b/x-pack/test/security_solution_cypress/cypress/README.md index 0a2b2f4c1c1a9..16a93f0c3b5d5 100644 --- a/x-pack/test/security_solution_cypress/cypress/README.md +++ b/x-pack/test/security_solution_cypress/cypress/README.md @@ -62,14 +62,13 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress | Runs the default Cypress command | | cypress:open:ess | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a local kibana and ES instance. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | | cypress:open:serverless | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a mocked serverless environment. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | -| cypress:run:ess | Runs all tests tagged as ESS placed in the `e2e` directory excluding `investigations`,`explore` and `detection_response` directories in headless mode | +| cypress:run:entity_analytics:ess | Runs all tests tagged as ESS placed in the `e2e/entity_analytics` directory in headless mode | | cypress:run:cases:ess | Runs all tests under `explore/cases` in the `e2e` directory related to the Cases area team in headless mode | | cypress:ess | Runs all ESS tests with the specified configuration in headless mode and produces a report using `cypress-multi-reporters` | - | cypress:rule_management:run:ess | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | | cypress:rule_management:prebuilt_rules:run:ess | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | | cypress:run:respops:ess | Runs all tests related to the Response Ops area team, specifically tests in `detection_alerts`, `detection_rules`, and `exceptions` directories in headless mode | -| cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations`, `explore` and `detections_response` directories in headless mode | +| cypress:run:entity_analytics:serverless | Runs all tests tagged as SERVERLESS in the `e2e/entity_analytics` directory in headless mode | | cypress:rule_management:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | | cypress:rule_management:prebuilt_rules:run:serverless | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | | cypress:detection_engine:run:ess | Runs all tests tagged as ESS in the `e2e/detection_response/detection_engine` excluding `e2e/detection_response/detection_engine/exceptions` directory in headless mode | @@ -83,7 +82,7 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress:investigations:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode | | cypress:open:qa:serverless | Opens the Cypress UI with all tests in the `e2e` directory tagged as SERVERLESS. This also creates an MKI project in console.qa enviornment. The kibana instance will reload when you make code changes. This is the recommended way to debug tests in QA. Follow the readme in order to learn about the known limitations. | -| cypress:run:qa:serverless | Runs all tests tagged as SERVERLESS placed in the `e2e` directory excluding `investigations`, `explore` and `rule_management` directories in headless mode using the QA environment and real MKI projects.| +| cypress:run:qa:serverless:entity_analytics | Runs all tests tagged as SERVERLESS placed in the `e2e/entity_analytics` directory in headless mode using the QA environment and real MKI projects.| | cypress:run:qa:serverless:explore | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode using the QA environment and real MKI prorjects. | | cypress:run:qa:serverless:investigations | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode using the QA environment and reak MKI projects. | | cypress:run:qa:serverless:rule_management | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` directory, excluding `e2e/detection_response/rule_management/prebuilt_rules` in headless mode using the QA environment and reak MKI projects. | @@ -123,6 +122,8 @@ If you belong to one of the teams listed in the table, please add new e2e specs | `e2e/detection_response/rule_management` | Detection Rule Management | | `e2e/detection_response/detection_engine` | Detection Engine | | `e2e/ai_assistant` | AI Assistant | +| `e2e/entity_analytics` | Entity Analytics | + ### fixtures/ @@ -219,7 +220,7 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | Script Name | Description | | ----------- | ----------- | | cypress:open:serverless | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a mocked serverless environment. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | -| cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | +| cypress:entity_analytics:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/entity_analytics` directory in headless mode | | cypress:investigations:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode | | cypress:rule_management:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/enable_risk_score_redirect.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/enable_risk_score_redirect.cy.ts similarity index 100% rename from x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/enable_risk_score_redirect.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/enable_risk_score_redirect.cy.ts diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/entity_analytics.cy.ts similarity index 100% rename from x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/entity_analytics.cy.ts diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics_serverless_splash_screen.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/entity_analytics_serverless_splash_screen.cy.ts similarity index 100% rename from x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics_serverless_splash_screen.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/entity_analytics_serverless_splash_screen.cy.ts diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/upgrade_risk_score.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/upgrade_risk_score.cy.ts similarity index 100% rename from x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/upgrade_risk_score.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/upgrade_risk_score.cy.ts diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/host_details/risk_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/host_details/risk_tab.cy.ts similarity index 100% rename from x-pack/test/security_solution_cypress/cypress/e2e/explore/host_details/risk_tab.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/host_details/risk_tab.cy.ts diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/host_risk_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/hosts/host_risk_tab.cy.ts similarity index 100% rename from x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/host_risk_tab.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/hosts/host_risk_tab.cy.ts diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/hosts_risk_column.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/hosts/hosts_risk_column.cy.ts similarity index 100% rename from x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/hosts_risk_column.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/hosts/hosts_risk_column.cy.ts diff --git a/x-pack/test/security_solution_cypress/package.json b/x-pack/test/security_solution_cypress/package.json index 04aff67e6b8ce..6b366061b381c 100644 --- a/x-pack/test/security_solution_cypress/package.json +++ b/x-pack/test/security_solution_cypress/package.json @@ -7,7 +7,7 @@ "scripts": { "cypress": "NODE_OPTIONS=--openssl-legacy-provider ../../../node_modules/.bin/cypress", "cypress:open:ess": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel open --spec './cypress/e2e/**/*.cy.ts' --config-file ../../test/security_solution_cypress/cypress/cypress.config.ts --ftr-config-file ../../test/security_solution_cypress/cli_config", - "cypress:run:ess": "yarn cypress:ess --spec './cypress/e2e/!(investigations|explore|detection_response|ai_assistant)/**/*.cy.ts'", + "cypress:entity_analytics:run:ess": "yarn cypress:ess --spec './cypress/e2e/entity_analytics/**/*.cy.ts'", "cypress:run:cases:ess": "yarn cypress:ess --spec './cypress/e2e/explore/cases/*.cy.ts'", "cypress:ess": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel run --config-file ../../test/security_solution_cypress/cypress/cypress_ci.config.ts --ftr-config-file ../../test/security_solution_cypress/cli_config", "cypress:rule_management:run:ess":"yarn cypress:ess --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", @@ -26,8 +26,7 @@ "cypress:cloud:serverless": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider NODE_TLS_REJECT_UNAUTHORIZED=0 ../../../node_modules/.bin/cypress", "cypress:open:cloud:serverless": "yarn cypress:cloud:serverless open --config-file ./cypress/cypress_serverless.config.ts --env CLOUD_SERVERLESS=true", "cypress:open:serverless": "yarn cypress:serverless open --config-file ../../test/security_solution_cypress/cypress/cypress_serverless.config.ts --spec './cypress/e2e/**/*.cy.ts'", - "cypress:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/!(investigations|explore|detection_response|ai_assistant)/**/*.cy.ts'", - "cypress:run:cloud:serverless": "yarn cypress:cloud:serverless run --config-file ./cypress/cypress_ci_serverless.config.ts --env CLOUD_SERVERLESS=true", + "cypress:entity_analytics:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/entity_analytics/**/*.cy.ts'", "cypress:rule_management:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", "cypress:rule_management:prebuilt_rules:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/detection_response/rule_management/prebuilt_rules/**/*.cy.ts'", "cypress:detection_engine:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/detection_response/detection_engine/!(exceptions)/**/*.cy.ts'", @@ -39,7 +38,7 @@ "cypress:burn:serverless": "yarn cypress:serverless --env burn=2", "cypress:qa:serverless": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel_serverless --config-file ../../test/security_solution_cypress/cypress/cypress_ci_serverless_qa.config.ts", "cypress:open:qa:serverless": "yarn cypress:qa:serverless open", - "cypress:run:qa:serverless": "yarn cypress:qa:serverless --spec './cypress/e2e/!(investigations|explore|detection_response|ai_assistant)/**/*.cy.ts'", + "cypress:run:qa:serverless:entity_analytics": "yarn cypress:qa:serverless --spec './cypress/e2e/entity_analytics/**/*.cy.ts'", "cypress:run:qa:serverless:investigations": "yarn cypress:qa:serverless --spec './cypress/e2e/investigations/**/*.cy.ts'", "cypress:run:qa:serverless:explore": "yarn cypress:qa:serverless --spec './cypress/e2e/explore/**/*.cy.ts'", "cypress:run:qa:serverless:rule_management": "yarn cypress:qa:serverless --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index c1eed560a2142..f1edd7a45800c 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -117,8 +117,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.endpoint.navigateToEndpointList(); }); - // FLAKY: https://github.com/elastic/kibana/issues/170357 - describe.skip('when there is data,', () => { + describe('when there is data,', () => { before(async () => { indexedData = await endpointTestResources.loadEndpointData({ numHosts: 3 }); await pageObjects.endpoint.navigateToEndpointList(); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts index bcc3177846d37..6f00806e67c05 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts @@ -82,7 +82,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }; - describe('Response Actions Responder', function () { + // FLAKY: https://github.com/elastic/kibana/issues/153071 + describe.skip('Response Actions Responder', function () { targetTags(this, ['@ess', '@serverless']); let indexedData: IndexedHostsAndAlertsResponse; diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts index ae886a0d1a4d5..802cbf8be16fc 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts @@ -51,19 +51,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true'); }; - // Several flaky tests from this file in serverless, hence @skipInServerless - // - https://github.com/elastic/kibana/issues?q=is%3Aissue+is%3Aopen+X-pack+endpoint+integrations++artifact+entries+list - // https://github.com/elastic/kibana/issues/171475 - // https://github.com/elastic/kibana/issues/171476 - // https://github.com/elastic/kibana/issues/171477 - // https://github.com/elastic/kibana/issues/171478 - // https://github.com/elastic/kibana/issues/171487 - // https://github.com/elastic/kibana/issues/171488 - // https://github.com/elastic/kibana/issues/171489 - // https://github.com/elastic/kibana/issues/171491 - // https://github.com/elastic/kibana/issues/171492 describe('For each artifact list under management', function () { - targetTags(this, ['@ess', '@serverless', '@skipInServerless']); + targetTags(this, ['@ess', '@serverless']); this.timeout(60_000 * 5); let indexedData: IndexedHostsAndAlertsResponse; @@ -225,9 +214,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }; for (const testData of getArtifactsListTestsData()) { - // FLAKY: https://github.com/elastic/kibana/issues/171489 - // FLAKY: https://github.com/elastic/kibana/issues/171475 - describe.skip(`When on the ${testData.title} entries list`, function () { + describe(`When on the ${testData.title} entries list`, function () { beforeEach(async () => { policyInfo = await policyTestResources.createPolicy(); await removeAllArtifacts(); @@ -313,8 +300,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - // FLAKY: https://github.com/elastic/kibana/issues/171476 - describe.skip('Should check artifacts are correctly generated when multiple entries', function () { + describe('Should check artifacts are correctly generated when multiple entries', function () { let firstPolicy: PolicyTestResourceInfo; let secondPolicy: PolicyTestResourceInfo; diff --git a/x-pack/test/security_solution_endpoint/services/endpoint.ts b/x-pack/test/security_solution_endpoint/services/endpoint.ts index 1372ffe0139fd..b4b5e5aef96bf 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint.ts @@ -9,6 +9,7 @@ import { errors } from '@elastic/elasticsearch'; import { Client } from '@elastic/elasticsearch'; +import { AGENTS_INDEX } from '@kbn/fleet-plugin/common'; import { metadataCurrentIndexPattern, metadataTransformPrefix, @@ -94,7 +95,7 @@ export class EndpointTestResources extends FtrService { * @param [options.enableFleetIntegration=true] When set to `true`, Fleet data will also be loaded (ex. Integration Policies, Agent Policies, "fake" Agents) * @param [options.generatorSeed='seed`] The seed to be used by the data generator. Important in order to ensure the same data is generated on very run. * @param [options.waitUntilTransformed=true] If set to `true`, the data loading process will wait until the endpoint hosts metadata is processed by the transform - * @param [options.waitTimeout=60000] If waitUntilTransformed=true, number of ms to wait until timeout + * @param [options.waitTimeout=120000] If waitUntilTransformed=true, number of ms to wait until timeout * @param [options.customIndexFn] If provided, will use this function to generate and index data instead */ async loadEndpointData( @@ -116,7 +117,7 @@ export class EndpointTestResources extends FtrService { enableFleetIntegration = true, generatorSeed = 'seed', waitUntilTransformed = true, - waitTimeout = 60000, + waitTimeout = 120000, customIndexFn, } = options; @@ -197,6 +198,12 @@ export class EndpointTestResources extends FtrService { await this.retry.waitForWithTimeout(`endpoint hosts in ${index}`, timeout, async () => { try { + if (index === METADATA_UNITED_INDEX) { + // United metadata transform occasionally can't find docs in .fleet-agents. + // Running a search on the index first eliminates this issue. + // Replacing the search with a refresh does not resolve flakiness. + await this.esClient.search({ index: AGENTS_INDEX }); + } const searchResponse = await this.esClient.search({ index, size, diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 3ac6c83938c14..0e118cf88e4b9 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -47,8 +47,7 @@ export default function ({ getService }: FtrProviderContext) { const endpointTestResources = getService('endpointTestResources'); const log = getService('log'); - // Failing: See https://github.com/elastic/kibana/issues/151854 - describe.skip('test metadata apis', function () { + describe('test metadata apis', function () { targetTags(this, ['@ess', '@serverless']); describe('list endpoints GET route', () => { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index cd9bde5d36a73..e5c89162f923d 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", - "types": ["node", "@kbn/ambient-ftr-types"], + "types": [ + "node", + "@kbn/ambient-ftr-types" + ], // there is still a decent amount of JS in this plugin and we are taking // advantage of the fact that TS doesn't know the types of that code and // gives us `any`. Once that code is converted to .ts we can remove this @@ -18,9 +21,18 @@ "../../typings/**/*", "../../packages/kbn-test/types/ftr_globals/**/*" ], - "exclude": ["security_solution_cypress/cypress/**/*", "target/**/*", "*/plugins/**/*", "*/packages/**/*", "*/*/packages/**/*","security_solution_api_integration/**/*" ], + "exclude": [ + "security_solution_cypress/cypress/**/*", + "target/**/*", + "*/plugins/**/*", + "*/packages/**/*", + "*/*/packages/**/*", + "security_solution_api_integration/**/*" + ], "kbn_references": [ - { "path": "../../test/tsconfig.json" }, + { + "path": "../../test/tsconfig.json" + }, "@kbn/core", "@kbn/data-plugin", "@kbn/kibana-usage-collection-plugin", @@ -149,6 +161,9 @@ "@kbn/reporting-export-types-pdf-common", "@kbn/reporting-export-types-png-common", "@kbn/reporting-common", + "@kbn/observability-log-explorer-plugin", + "@kbn/io-ts-utils", + "@kbn/log-explorer-plugin", "@kbn/security-plugin-types-common", ] } diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts index a011a00a24f9c..d19cb269891d7 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts @@ -5,7 +5,6 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; import { FtrProviderContext } from '../../../ftr_provider_context'; const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'message']; @@ -41,10 +40,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should restore the table columns from the URL state if exists', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ - columns: ['service.name', 'host.name', 'message', 'data_stream.namespace'], - }), + pageState: { + columns: [ + { field: 'service.name' }, + { field: 'host.name' }, + { field: 'message' }, + { field: 'data_stream.namespace' }, + ], }, }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts index 9f6e978e1f270..36a2451f4115d 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts @@ -5,9 +5,23 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; +import { decodeOrThrow, indexPatternRt } from '@kbn/io-ts-utils'; +import { DatasetSelectionPlain } from '@kbn/log-explorer-plugin/common'; import { FtrProviderContext } from '../../../ftr_provider_context'; +const azureActivityDatasetSelection: DatasetSelectionPlain = { + selection: { + dataset: { + name: decodeOrThrow(indexPatternRt)('logs-azure.activitylogs-*'), + title: 'activitylogs', + }, + name: 'azure', + title: 'Azure Logs', + version: '1.5.23', + }, + selectionType: 'single', +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const retry = getService('retry'); @@ -27,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.svlCommonPage.forceLogout(); }); - describe('when the "index" query param does not exist', () => { + describe('when no dataset selection is given', () => { it('should initialize the "All logs" selection', async () => { await PageObjects.observabilityLogExplorer.navigateTo(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -38,13 +52,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('when the "index" query param exists', () => { + describe('when a dataset selection is given', () => { it('should decode and restore the selection from a valid encoded index', async () => { - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -56,10 +68,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should fallback to the "All logs" selection and notify the user of an invalid encoded index', async () => { - const invalidEncodedIndex = 'invalid-encoded-index'; - await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: invalidEncodedIndex }), + await PageObjects.observabilityLogExplorer.navigateToWithUncheckedState({ + pageState: { + v: 1, + datasetSelection: { + selectionType: 'invalid', + }, }, }); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -80,12 +94,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(allDatasetSelectionTitle).to.be('All logs'); - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), - controlPanels: rison.encode({}), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts index 79f241c8948b6..d33d9e97d18c6 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts @@ -50,8 +50,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts index 98a3914fed4e8..dd19471c5620d 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts @@ -70,8 +70,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -129,8 +134,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -193,8 +203,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -260,8 +275,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index af6f6e4b25e99..72039a6ad1368 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -2,8 +2,18 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", - "rootDirs": [".", "../test"], - "types": ["node", "@kbn/ambient-ftr-types", "cypress", "cypress-file-upload", "cypress-real-events", "cypress-recurse"], + "rootDirs": [ + ".", + "../test" + ], + "types": [ + "node", + "@kbn/ambient-ftr-types", + "cypress", + "cypress-file-upload", + "cypress-real-events", + "cypress-recurse" + ], }, "include": [ "**/*", @@ -17,7 +27,9 @@ "*/*/packages/**/*", ], "kbn_references": [ - { "path": "../test/tsconfig.json" }, + { + "path": "../test/tsconfig.json" + }, "@kbn/expect", "@kbn/test", "@kbn/repo-info", @@ -68,6 +80,8 @@ "@kbn/apm-synthtrace-client", "@kbn/reporting-export-types-csv-common", "@kbn/mock-idp-plugin", + "@kbn/io-ts-utils", + "@kbn/log-explorer-plugin", "@kbn/index-management-plugin", ] }