From c6dd76e76c4bad4c6f0a78dda86b0cc67eb31ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georgiana-Andreea=20Onolea=C8=9B=C4=83?= Date: Tue, 8 Oct 2024 16:10:24 +0300 Subject: [PATCH 01/50] [ResponseOps][Cases]Design Review changes PR 3 (#194936) ## Summary Closes https://github.com/elastic/kibana/issues/188187 - The fly out title now updates appropriately when editing an existing field or template, displaying "Edit field" and "Edit template" respectively. https://github.com/user-attachments/assets/b85d7d27-ba89-4fbd-97c4-dc5873dda6d8 --- .../components/configure_cases/index.test.tsx | 12 ++++++++++++ .../public/components/configure_cases/index.tsx | 8 ++++++-- .../components/configure_cases/translations.ts | 13 ++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 7f375ef75938bc..6c65eae41c78bc 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -902,6 +902,10 @@ describe('ConfigureCases', () => { expect(await screen.findByTestId('common-flyout')).toBeInTheDocument(); + expect(await screen.findByTestId('common-flyout-header')).toHaveTextContent( + i18n.EDIT_CUSTOM_FIELD + ); + await userEvent.click(screen.getByTestId('custom-field-label-input')); await userEvent.paste('!!'); await userEvent.click(screen.getByTestId('text-custom-field-required')); @@ -941,6 +945,10 @@ describe('ConfigureCases', () => { await userEvent.click(screen.getByTestId('add-custom-field')); expect(await screen.findByTestId('common-flyout')).toBeInTheDocument(); + + expect(await screen.findByTestId('common-flyout-header')).toHaveTextContent( + i18n.ADD_CUSTOM_FIELD + ); }); it('closes fly out for when click on cancel', async () => { @@ -1177,6 +1185,10 @@ describe('ConfigureCases', () => { expect(await screen.findByTestId('common-flyout')).toBeInTheDocument(); + expect(await screen.findByTestId('common-flyout-header')).toHaveTextContent( + i18n.EDIT_TEMPLATE + ); + await userEvent.clear(await screen.findByTestId('template-name-input')); await userEvent.click(await screen.findByTestId('template-name-input')); await userEvent.paste('Updated template name'); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index d26fe1ee185f84..61f99a46a0b08c 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -484,7 +484,9 @@ export const ConfigureCases: React.FC = React.memo(() => { } onCloseFlyout={onCloseCustomFieldFlyout} onSaveField={onCustomFieldSave} - renderHeader={() => {i18n.ADD_CUSTOM_FIELD}} + renderHeader={() => ( + {customFieldToEdit ? i18n.EDIT_CUSTOM_FIELD : i18n.ADD_CUSTOM_FIELD} + )} > {({ onChange }) => ( @@ -504,7 +506,9 @@ export const ConfigureCases: React.FC = React.memo(() => { } onCloseFlyout={onCloseTemplateFlyout} onSaveField={onTemplateSave} - renderHeader={() => {i18n.CREATE_TEMPLATE}} + renderHeader={() => ( + {templateToEdit ? i18n.EDIT_TEMPLATE : i18n.CREATE_TEMPLATE} + )} > {({ onChange }) => ( Date: Tue, 8 Oct 2024 15:13:37 +0200 Subject: [PATCH 02/50] [Search] Add UI kbn feature flag for Inference Endpoints (#195385) This adds a UI feature flag for the inference endpoints plugin. Call this to enable: ``` POST kbn:/internal/kibana/settings/inferenceEndpointsUi:enabled {"value": true} ``` --- x-pack/plugins/search_inference_endpoints/public/index.ts | 2 ++ .../plugins/search_inference_endpoints/public/plugin.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/search_inference_endpoints/public/index.ts b/x-pack/plugins/search_inference_endpoints/public/index.ts index b06f1f64b909da..1c9e267a18bdec 100644 --- a/x-pack/plugins/search_inference_endpoints/public/index.ts +++ b/x-pack/plugins/search_inference_endpoints/public/index.ts @@ -17,3 +17,5 @@ export type { SearchInferenceEndpointsPluginSetup, SearchInferenceEndpointsPluginStart, } from './types'; + +export const INFERENCE_ENDPOINTS_UI_FLAG = 'inferenceEndpointsUi:enabled'; diff --git a/x-pack/plugins/search_inference_endpoints/public/plugin.ts b/x-pack/plugins/search_inference_endpoints/public/plugin.ts index ab49281f82e4c4..a864b7c0fcdd62 100644 --- a/x-pack/plugins/search_inference_endpoints/public/plugin.ts +++ b/x-pack/plugins/search_inference_endpoints/public/plugin.ts @@ -21,6 +21,7 @@ import { SearchInferenceEndpointsPluginSetup, SearchInferenceEndpointsPluginStart, } from './types'; +import { INFERENCE_ENDPOINTS_UI_FLAG } from '.'; export class SearchInferenceEndpointsPlugin implements Plugin @@ -34,8 +35,11 @@ export class SearchInferenceEndpointsPlugin public setup( core: CoreSetup ): SearchInferenceEndpointsPluginSetup { - if (!this.config.ui?.enabled) return {}; - + if ( + !this.config.ui?.enabled && + !core.uiSettings.get(INFERENCE_ENDPOINTS_UI_FLAG, false) + ) + return {}; core.application.register({ id: PLUGIN_ID, appRoute: '/app/search_inference_endpoints', From cb9f998c064f4519afd9e7bbf73c9c55ef7059b3 Mon Sep 17 00:00:00 2001 From: Ash <1849116+ashokaditya@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:17:12 +0200 Subject: [PATCH 03/50] [DataUsage] Add internal API/UX hooks to interact with serverless project metrics API (#193966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Screenshot 2024-10-07 at 10 28 51 AM Adds the bulk of the code for new [Data Usage plugin](https://github.com/elastic/kibana/pull/193466). Plugin is still disabled by default and will be enabled later for serverless only. - UI components (filters, charts) - Adds an internal API route that provides visualization data to render data usage charts for serverless. ### request example ```json5 GET /internal/api/data_usage/metrics?from=1726858530000&to=1726908930000&size=0&metricTypes=ingest_rate&metricTypes=storage_retained&dataStreams=ds_name_1&dataStreams=ds_name_2 ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: neptunian Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../cards_navigation/src/consts.tsx | 2 +- x-pack/plugins/data_usage/common/index.ts | 4 + .../data_usage/common/query_client.tsx | 57 +++++ .../rest_types/data_streams.ts} | 18 +- .../data_usage/common/rest_types/index.ts | 9 + .../common/rest_types/usage_metrics.test.ts | 179 +++++++++++++ .../common/rest_types/usage_metrics.ts | 102 ++++++++ x-pack/plugins/data_usage/common/types.ts | 9 + x-pack/plugins/data_usage/kibana.jsonc | 20 +- .../public/app/components/chart_panel.tsx | 123 +++++++++ .../public/app/components/charts.tsx | 36 +++ .../app/components/dataset_quality_link.tsx | 44 ++++ .../public/app/components/date_picker.tsx | 88 +++++++ .../public/app/components/legend_action.tsx | 89 +++++++ .../data_usage/public/app/data_usage.tsx | 147 +++++++++++ .../app/hooks/use_charts_url_params.tsx | 136 ++++++++++ .../public/app/hooks/use_date_picker.tsx | 98 ++++++++ x-pack/plugins/data_usage/public/app/types.ts | 24 ++ .../plugins/data_usage/public/application.tsx | 8 +- .../public/hooks/use_get_usage_metrics.ts | 45 ++++ .../data_usage/public/hooks/use_url_params.ts | 30 +++ .../public/utils/use_breadcrumbs.tsx | 28 +++ .../data_usage/server/common/errors.ts | 18 ++ x-pack/plugins/data_usage/server/config.ts | 13 +- x-pack/plugins/data_usage/server/index.ts | 8 +- x-pack/plugins/data_usage/server/plugin.ts | 42 +++- .../data_usage/server/routes/error_handler.ts | 40 +++ .../data_usage/server/routes/index.tsx | 17 ++ .../server/routes/internal/data_streams.ts | 37 +++ .../routes/internal/data_streams_handler.ts | 43 ++++ .../server/routes/internal/index.tsx | 9 + .../server/routes/internal/usage_metrics.ts | 37 +++ .../routes/internal/usage_metrics_handler.ts | 237 ++++++++++++++++++ .../plugins/data_usage/server/types/index.ts | 8 + .../plugins/data_usage/server/types/types.ts | 42 ++++ .../server/utils/custom_http_request_error.ts | 22 ++ .../plugins/data_usage/server/utils/index.ts | 8 + x-pack/plugins/data_usage/tsconfig.json | 7 + 38 files changed, 1859 insertions(+), 25 deletions(-) create mode 100644 x-pack/plugins/data_usage/common/query_client.tsx rename x-pack/plugins/data_usage/{server/types.ts => common/rest_types/data_streams.ts} (51%) create mode 100644 x-pack/plugins/data_usage/common/rest_types/index.ts create mode 100644 x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts create mode 100644 x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts create mode 100644 x-pack/plugins/data_usage/common/types.ts create mode 100644 x-pack/plugins/data_usage/public/app/components/chart_panel.tsx create mode 100644 x-pack/plugins/data_usage/public/app/components/charts.tsx create mode 100644 x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx create mode 100644 x-pack/plugins/data_usage/public/app/components/date_picker.tsx create mode 100644 x-pack/plugins/data_usage/public/app/components/legend_action.tsx create mode 100644 x-pack/plugins/data_usage/public/app/data_usage.tsx create mode 100644 x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx create mode 100644 x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx create mode 100644 x-pack/plugins/data_usage/public/app/types.ts create mode 100644 x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts create mode 100644 x-pack/plugins/data_usage/public/hooks/use_url_params.ts create mode 100644 x-pack/plugins/data_usage/public/utils/use_breadcrumbs.tsx create mode 100644 x-pack/plugins/data_usage/server/common/errors.ts create mode 100644 x-pack/plugins/data_usage/server/routes/error_handler.ts create mode 100644 x-pack/plugins/data_usage/server/routes/index.tsx create mode 100644 x-pack/plugins/data_usage/server/routes/internal/data_streams.ts create mode 100644 x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts create mode 100644 x-pack/plugins/data_usage/server/routes/internal/index.tsx create mode 100644 x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts create mode 100644 x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts create mode 100644 x-pack/plugins/data_usage/server/types/index.ts create mode 100644 x-pack/plugins/data_usage/server/types/types.ts create mode 100644 x-pack/plugins/data_usage/server/utils/custom_http_request_error.ts create mode 100644 x-pack/plugins/data_usage/server/utils/index.ts diff --git a/packages/kbn-management/cards_navigation/src/consts.tsx b/packages/kbn-management/cards_navigation/src/consts.tsx index 16e655c5510ad0..6a22b9e33d620e 100644 --- a/packages/kbn-management/cards_navigation/src/consts.tsx +++ b/packages/kbn-management/cards_navigation/src/consts.tsx @@ -77,7 +77,7 @@ export const appDefinitions: Record = { description: i18n.translate('management.landing.withCardNavigation.dataUsageDescription', { defaultMessage: 'View data usage and retention.', }), - icon: 'documents', + icon: 'stats', }, [AppIds.RULES]: { diff --git a/x-pack/plugins/data_usage/common/index.ts b/x-pack/plugins/data_usage/common/index.ts index 4b6f899b58d377..eb0787f53f344e 100644 --- a/x-pack/plugins/data_usage/common/index.ts +++ b/x-pack/plugins/data_usage/common/index.ts @@ -11,3 +11,7 @@ export const PLUGIN_ID = 'data_usage'; export const PLUGIN_NAME = i18n.translate('xpack.dataUsage.name', { defaultMessage: 'Data Usage', }); + +export const DATA_USAGE_API_ROUTE_PREFIX = '/api/data_usage/'; +export const DATA_USAGE_METRICS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}metrics`; +export const DATA_USAGE_DATA_STREAMS_API_ROUTE = `/internal${DATA_USAGE_API_ROUTE_PREFIX}data_streams`; diff --git a/x-pack/plugins/data_usage/common/query_client.tsx b/x-pack/plugins/data_usage/common/query_client.tsx new file mode 100644 index 00000000000000..8a64ed7a51349a --- /dev/null +++ b/x-pack/plugins/data_usage/common/query_client.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PropsWithChildren } from 'react'; +import React, { memo, useMemo } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +type QueryClientOptionsProp = ConstructorParameters[0]; + +/** + * Default Query Client for Data Usage. + */ +export class DataUsageQueryClient extends QueryClient { + constructor(options: QueryClientOptionsProp = {}) { + const optionsWithDefaults: QueryClientOptionsProp = { + ...options, + defaultOptions: { + ...(options.defaultOptions ?? {}), + queries: { + refetchIntervalInBackground: false, + refetchOnWindowFocus: false, + refetchOnMount: true, + keepPreviousData: true, + ...(options?.defaultOptions?.queries ?? {}), + }, + }, + }; + super(optionsWithDefaults); + } +} + +/** + * The default Data Usage Query Client. Can be imported and used from outside of React hooks + * and still benefit from ReactQuery features (like caching, etc) + * + * @see https://tanstack.com/query/v4/docs/reference/QueryClient + */ +export const dataUsageQueryClient = new DataUsageQueryClient(); + +export type ReactQueryClientProviderProps = PropsWithChildren<{ + queryClient?: DataUsageQueryClient; +}>; + +export const DataUsageReactQueryClientProvider = memo( + ({ queryClient, children }) => { + const client = useMemo(() => { + return queryClient || dataUsageQueryClient; + }, [queryClient]); + return {children}; + } +); + +DataUsageReactQueryClientProvider.displayName = 'DataUsageReactQueryClientProvider'; diff --git a/x-pack/plugins/data_usage/server/types.ts b/x-pack/plugins/data_usage/common/rest_types/data_streams.ts similarity index 51% rename from x-pack/plugins/data_usage/server/types.ts rename to x-pack/plugins/data_usage/common/rest_types/data_streams.ts index 9f43ae2d3c2983..b1c02bb40854d1 100644 --- a/x-pack/plugins/data_usage/server/types.ts +++ b/x-pack/plugins/data_usage/common/rest_types/data_streams.ts @@ -5,12 +5,14 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/no-empty-interface*/ +import { schema } from '@kbn/config-schema'; -export interface DataUsageSetupDependencies {} - -export interface DataUsageStartDependencies {} - -export interface DataUsageServerSetup {} - -export interface DataUsageServerStart {} +export const DataStreamsResponseSchema = { + body: () => + schema.arrayOf( + schema.object({ + name: schema.string(), + storageSizeBytes: schema.number(), + }) + ), +}; diff --git a/x-pack/plugins/data_usage/common/rest_types/index.ts b/x-pack/plugins/data_usage/common/rest_types/index.ts new file mode 100644 index 00000000000000..64b5c640ebbb5f --- /dev/null +++ b/x-pack/plugins/data_usage/common/rest_types/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 './usage_metrics'; +export * from './data_streams'; diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts new file mode 100644 index 00000000000000..f6c08e2caddc00 --- /dev/null +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts @@ -0,0 +1,179 @@ +/* + * 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 { UsageMetricsRequestSchema } from './usage_metrics'; + +describe('usage_metrics schemas', () => { + it('should accept valid request query', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ['storage_retained'], + }) + ).not.toThrow(); + }); + + it('should accept a single `metricTypes` in request query', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: 'ingest_rate', + }) + ).not.toThrow(); + }); + + it('should accept multiple `metricTypes` in request query', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ['ingest_rate', 'storage_retained', 'index_rate'], + }) + ).not.toThrow(); + }); + + it('should accept a single string as `dataStreams` in request query', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: 'storage_retained', + dataStreams: 'data_stream_1', + }) + ).not.toThrow(); + }); + + it('should accept `dataStream` list', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ['storage_retained'], + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], + }) + ).not.toThrow(); + }); + + it('should error if `dataStream` list is empty', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ['storage_retained'], + dataStreams: [], + }) + ).toThrowError('expected value of type [string] but got [Array]'); + }); + + it('should error if `dataStream` is given an empty string', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ['storage_retained'], + dataStreams: ' ', + }) + ).toThrow('[dataStreams] must have at least one value'); + }); + + it('should error if `dataStream` is given an empty item in the list', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ['storage_retained'], + dataStreams: ['ds_1', ' '], + }) + ).toThrow('[dataStreams] list can not contain empty values'); + }); + + it('should error if `metricTypes` is empty string', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ' ', + }) + ).toThrow(); + }); + + it('should error if `metricTypes` is empty item', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: [' ', 'storage_retained'], + }) + ).toThrow('[metricTypes] list can not contain empty values'); + }); + + it('should error if `metricTypes` is not a valid value', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: 'foo', + }) + ).toThrow( + '[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate' + ); + }); + + it('should error if `metricTypes` is not a valid list', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: new Date().toISOString(), + metricTypes: ['storage_retained', 'foo'], + }) + ).toThrow( + '[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate' + ); + }); + + it('should error if `from` is not a valid input', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: 1010, + to: new Date().toISOString(), + metricTypes: ['storage_retained', 'foo'], + }) + ).toThrow('[from]: expected value of type [string] but got [number]'); + }); + + it('should error if `to` is not a valid input', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: 1010, + metricTypes: ['storage_retained', 'foo'], + }) + ).toThrow('[to]: expected value of type [string] but got [number]'); + }); + + it('should error if `from` is empty string', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: ' ', + to: new Date().toISOString(), + metricTypes: ['storage_retained', 'foo'], + }) + ).toThrow('[from]: Date ISO string must not be empty'); + }); + + it('should error if `to` is empty string', () => { + expect(() => + UsageMetricsRequestSchema.query.validate({ + from: new Date().toISOString(), + to: ' ', + metricTypes: ['storage_retained', 'foo'], + }) + ).toThrow('[to]: Date ISO string must not be empty'); + }); +}); diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts new file mode 100644 index 00000000000000..f2bbdb616fc798 --- /dev/null +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts @@ -0,0 +1,102 @@ +/* + * 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, type TypeOf } from '@kbn/config-schema'; + +const METRIC_TYPE_VALUES = [ + 'storage_retained', + 'ingest_rate', + 'search_vcu', + 'ingest_vcu', + 'ml_vcu', + 'index_latency', + 'index_rate', + 'search_latency', + 'search_rate', +] as const; + +export type MetricTypes = (typeof METRIC_TYPE_VALUES)[number]; + +// type guard for MetricTypes +export const isMetricType = (type: string): type is MetricTypes => + METRIC_TYPE_VALUES.includes(type as MetricTypes); + +// @ts-ignore +const isValidMetricType = (value: string) => METRIC_TYPE_VALUES.includes(value); + +const DateSchema = schema.string({ + minLength: 1, + validate: (v) => (v.trim().length ? undefined : 'Date ISO string must not be empty'), +}); + +const metricTypesSchema = schema.oneOf( + // @ts-expect-error TS2769: No overload matches this call + METRIC_TYPE_VALUES.map((metricType) => schema.literal(metricType)) // Create a oneOf schema for the keys +); +export const UsageMetricsRequestSchema = { + query: schema.object({ + from: DateSchema, + to: DateSchema, + metricTypes: schema.oneOf([ + schema.arrayOf(schema.string(), { + minSize: 1, + validate: (values) => { + if (values.map((v) => v.trim()).some((v) => !v.length)) { + return '[metricTypes] list can not contain empty values'; + } else if (values.map((v) => v.trim()).some((v) => !isValidMetricType(v))) { + return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; + } + }, + }), + schema.string({ + validate: (v) => { + if (!v.trim().length) { + return '[metricTypes] must have at least one value'; + } else if (!isValidMetricType(v)) { + return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; + } + }, + }), + ]), + dataStreams: schema.maybe( + schema.oneOf([ + schema.arrayOf(schema.string(), { + minSize: 1, + validate: (values) => { + if (values.map((v) => v.trim()).some((v) => !v.length)) { + return '[dataStreams] list can not contain empty values'; + } + }, + }), + schema.string({ + validate: (v) => + v.trim().length ? undefined : '[dataStreams] must have at least one value', + }), + ]) + ), + }), +}; + +export type UsageMetricsRequestSchemaQueryParams = TypeOf; + +export const UsageMetricsResponseSchema = { + body: () => + schema.object({ + metrics: schema.recordOf( + metricTypesSchema, + schema.arrayOf( + schema.object({ + name: schema.string(), + data: schema.arrayOf( + schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 }) // Each data point is an array of 2 numbers + ), + }) + ) + ), + }), +}; +export type UsageMetricsResponseSchemaBody = TypeOf; diff --git a/x-pack/plugins/data_usage/common/types.ts b/x-pack/plugins/data_usage/common/types.ts new file mode 100644 index 00000000000000..d80bae2458d09f --- /dev/null +++ b/x-pack/plugins/data_usage/common/types.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. + */ + +// temporary type until agreed on +export type MetricKey = 'ingestedMax' | 'retainedMax'; diff --git a/x-pack/plugins/data_usage/kibana.jsonc b/x-pack/plugins/data_usage/kibana.jsonc index 9b0f2d193925e1..ffd8833351267a 100644 --- a/x-pack/plugins/data_usage/kibana.jsonc +++ b/x-pack/plugins/data_usage/kibana.jsonc @@ -1,16 +1,28 @@ { "type": "plugin", "id": "@kbn/data-usage-plugin", - "owner": ["@elastic/obs-ai-assistant", "@elastic/security-solution"], + "owner": [ + "@elastic/obs-ai-assistant", + "@elastic/security-solution" + ], "plugin": { "id": "dataUsage", "server": true, "browser": true, - "configPath": ["xpack", "dataUsage"], - "requiredPlugins": ["home", "management", "features", "share"], + "configPath": [ + "xpack", + "dataUsage" + ], + "requiredPlugins": [ + "home", + "management", + "features", + "share" + ], "optionalPlugins": [], "requiredBundles": [ "kibanaReact", - ], + "data" + ] } } diff --git a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx new file mode 100644 index 00000000000000..c7937ae149de98 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useMemo } from 'react'; +import numeral from '@elastic/numeral'; +import { EuiFlexItem, EuiPanel, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { + Chart, + Axis, + BarSeries, + Settings, + ScaleType, + niceTimeFormatter, + DARK_THEME, + LIGHT_THEME, +} from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import { LegendAction } from './legend_action'; +import { MetricTypes } from '../../../common/rest_types'; +import { MetricSeries } from '../types'; + +// TODO: Remove this when we have a title for each metric type +type ChartKey = Extract; +export const chartKeyToTitleMap: Record = { + ingest_rate: i18n.translate('xpack.dataUsage.charts.ingestedMax', { + defaultMessage: 'Data Ingested', + }), + storage_retained: i18n.translate('xpack.dataUsage.charts.retainedMax', { + defaultMessage: 'Data Retained in Storage', + }), +}; + +interface ChartPanelProps { + metricType: MetricTypes; + series: MetricSeries[]; + idx: number; + popoverOpen: string | null; + togglePopover: (streamName: string | null) => void; +} + +export const ChartPanel: React.FC = ({ + metricType, + series, + idx, + popoverOpen, + togglePopover, +}) => { + const theme = useEuiTheme(); + + const chartTimestamps = series.flatMap((stream) => stream.data.map((d) => d[0])); + + const [minTimestamp, maxTimestamp] = [Math.min(...chartTimestamps), Math.max(...chartTimestamps)]; + + const tickFormat = useMemo( + () => niceTimeFormatter([minTimestamp, maxTimestamp]), + [minTimestamp, maxTimestamp] + ); + + const renderLegendAction = useCallback( + ({ label }: { label: string }) => { + return ( + + ); + }, + [idx, popoverOpen, togglePopover] + ); + return ( + + + +
{chartKeyToTitleMap[metricType as ChartKey] || metricType}
+
+ + + {series.map((stream, streamIdx) => ( + + ))} + + + + formatBytes(d)} + /> + +
+
+ ); +}; +const formatBytes = (bytes: number) => { + return numeral(bytes).format('0.0 b'); +}; diff --git a/x-pack/plugins/data_usage/public/app/components/charts.tsx b/x-pack/plugins/data_usage/public/app/components/charts.tsx new file mode 100644 index 00000000000000..6549f7e03830ae --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/charts.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useState } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; +import { MetricsResponse } from '../types'; +import { MetricTypes } from '../../../common/rest_types'; +import { ChartPanel } from './chart_panel'; +interface ChartsProps { + data: MetricsResponse; +} + +export const Charts: React.FC = ({ data }) => { + const [popoverOpen, setPopoverOpen] = useState(null); + const togglePopover = useCallback((streamName: string | null) => { + setPopoverOpen((prev) => (prev === streamName ? null : streamName)); + }, []); + + return ( + + {Object.entries(data.metrics).map(([metricType, series], idx) => ( + + ))} + + ); +}; diff --git a/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx b/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx new file mode 100644 index 00000000000000..d6627f3d8dca27 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/dataset_quality_link.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiListGroupItem } from '@elastic/eui'; +import { + DataQualityDetailsLocatorParams, + DATA_QUALITY_DETAILS_LOCATOR_ID, +} from '@kbn/deeplinks-observability'; +import { useKibanaContextForPlugin } from '../../utils/use_kibana'; +import { useDateRangePicker } from '../hooks/use_date_picker'; + +interface DatasetQualityLinkProps { + dataStreamName: string; +} + +export const DatasetQualityLink: React.FC = React.memo( + ({ dataStreamName }) => { + const { dateRangePickerState } = useDateRangePicker(); + const { + services: { + share: { url }, + }, + } = useKibanaContextForPlugin(); + const { startDate, endDate } = dateRangePickerState; + const locator = url.locators.get( + DATA_QUALITY_DETAILS_LOCATOR_ID + ); + const onClickDataQuality = async () => { + const locatorParams: DataQualityDetailsLocatorParams = { + dataStream: dataStreamName, + timeRange: { from: startDate, to: endDate, refresh: { pause: true, value: 0 } }, + }; + if (locator) { + await locator.navigate(locatorParams); + } + }; + return ; + } +); diff --git a/x-pack/plugins/data_usage/public/app/components/date_picker.tsx b/x-pack/plugins/data_usage/public/app/components/date_picker.tsx new file mode 100644 index 00000000000000..ca29acf8c96a6b --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/date_picker.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import type { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public'; +import type { EuiSuperDatePickerRecentRange } from '@elastic/eui'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { + DurationRange, + OnRefreshChangeProps, +} from '@elastic/eui/src/components/date_picker/types'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; + +export interface DateRangePickerValues { + autoRefreshOptions: { + enabled: boolean; + duration: number; + }; + startDate: string; + endDate: string; + recentlyUsedDateRanges: EuiSuperDatePickerRecentRange[]; +} + +interface UsageMetricsDateRangePickerProps { + dateRangePickerState: DateRangePickerValues; + isDataLoading: boolean; + onRefresh: () => void; + onRefreshChange: (evt: OnRefreshChangeProps) => void; + onTimeChange: ({ start, end }: DurationRange) => void; +} + +export const UsageMetricsDateRangePicker = memo( + ({ dateRangePickerState, isDataLoading, onRefresh, onRefreshChange, onTimeChange }) => { + const { euiTheme } = useEuiTheme(); + const kibana = useKibana(); + const { uiSettings } = kibana.services; + const [commonlyUsedRanges] = useState(() => { + return ( + uiSettings + ?.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES) + ?.map(({ from, to, display }: { from: string; to: string; display: string }) => { + return { + start: from, + end: to, + label: display, + }; + }) ?? [] + ); + }); + + return ( +
+ + + + + +
+ ); + } +); + +UsageMetricsDateRangePicker.displayName = 'UsageMetricsDateRangePicker'; diff --git a/x-pack/plugins/data_usage/public/app/components/legend_action.tsx b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx new file mode 100644 index 00000000000000..a816d1f8eaddab --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/components/legend_action.tsx @@ -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 React, { useCallback } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiPopover, + EuiListGroup, + EuiListGroupItem, + EuiSpacer, +} from '@elastic/eui'; +import { DatasetQualityLink } from './dataset_quality_link'; +import { useKibanaContextForPlugin } from '../../utils/use_kibana'; + +interface LegendActionProps { + idx: number; + popoverOpen: string | null; + togglePopover: (streamName: string | null) => void; + label: string; +} + +export const LegendAction: React.FC = React.memo( + ({ label, idx, popoverOpen, togglePopover }) => { + const uniqueStreamName = `${idx}-${label}`; + const { + services: { + share: { + url: { locators }, + }, + application: { capabilities }, + }, + } = useKibanaContextForPlugin(); + const hasDataSetQualityFeature = !!capabilities?.data_quality; + const hasIndexManagementFeature = !!capabilities?.index_management; + + const onClickIndexManagement = useCallback(async () => { + // TODO: use proper index management locator https://github.com/elastic/kibana/issues/195083 + const dataQualityLocator = locators.get('MANAGEMENT_APP_LOCATOR'); + if (dataQualityLocator) { + await dataQualityLocator.navigate({ + sectionId: 'data', + appId: `index_management/data_streams/${label}`, + }); + } + togglePopover(null); // Close the popover after action + }, [label, locators, togglePopover]); + + const onCopyDataStreamName = useCallback(() => { + navigator.clipboard.writeText(label); + togglePopover(null); // Close popover after copying + }, [label, togglePopover]); + + return ( + + + + togglePopover(uniqueStreamName)} + /> + + + } + isOpen={popoverOpen === uniqueStreamName} + closePopover={() => togglePopover(null)} + anchorPosition="downRight" + > + + + + + {hasIndexManagementFeature && ( + + )} + {hasDataSetQualityFeature && } + + + + ); + } +); diff --git a/x-pack/plugins/data_usage/public/app/data_usage.tsx b/x-pack/plugins/data_usage/public/app/data_usage.tsx new file mode 100644 index 00000000000000..c32f86d68b5bf8 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/data_usage.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingElastic, + EuiPageSection, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { UsageMetricsRequestSchemaQueryParams } from '../../common/rest_types'; +import { Charts } from './components/charts'; +import { UsageMetricsDateRangePicker } from './components/date_picker'; +import { useBreadcrumbs } from '../utils/use_breadcrumbs'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; +import { PLUGIN_NAME } from '../../common'; +import { useGetDataUsageMetrics } from '../hooks/use_get_usage_metrics'; +import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from './hooks/use_date_picker'; +import { useDataUsageMetricsUrlParams } from './hooks/use_charts_url_params'; +import { MetricsResponse } from './types'; + +export const DataUsage = () => { + const { + services: { chrome, appParams }, + } = useKibanaContextForPlugin(); + + const { + metricTypes: metricTypesFromUrl, + dataStreams: dataStreamsFromUrl, + startDate: startDateFromUrl, + endDate: endDateFromUrl, + setUrlMetricTypesFilter, + setUrlDateRangeFilter, + } = useDataUsageMetricsUrlParams(); + + const [queryParams, setQueryParams] = useState({ + metricTypes: ['storage_retained', 'ingest_rate'], + dataStreams: [], + from: DEFAULT_DATE_RANGE_OPTIONS.startDate, + to: DEFAULT_DATE_RANGE_OPTIONS.endDate, + }); + + useEffect(() => { + if (!metricTypesFromUrl) { + setUrlMetricTypesFilter( + typeof queryParams.metricTypes !== 'string' + ? queryParams.metricTypes.join(',') + : queryParams.metricTypes + ); + } + if (!startDateFromUrl || !endDateFromUrl) { + setUrlDateRangeFilter({ startDate: queryParams.from, endDate: queryParams.to }); + } + }, [ + endDateFromUrl, + metricTypesFromUrl, + queryParams.from, + queryParams.metricTypes, + queryParams.to, + setUrlDateRangeFilter, + setUrlMetricTypesFilter, + startDateFromUrl, + ]); + + useEffect(() => { + setQueryParams((prevState) => ({ + ...prevState, + metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, + dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, + })); + }, [metricTypesFromUrl, dataStreamsFromUrl]); + + const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); + + const { + error, + data, + isFetching, + isFetched, + refetch: refetchDataUsageMetrics, + } = useGetDataUsageMetrics( + { + ...queryParams, + from: dateRangePickerState.startDate, + to: dateRangePickerState.endDate, + }, + { + retry: false, + } + ); + + const onRefresh = useCallback(() => { + refetchDataUsageMetrics(); + }, [refetchDataUsageMetrics]); + + useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); + + // TODO: show a toast? + if (!isFetching && error?.body) { + return
{error.body.message}
; + } + + return ( + <> + +

+ {i18n.translate('xpack.dataUsage.pageTitle', { + defaultMessage: 'Data Usage', + })} +

+
+ + + + + + + + + + + + + + {isFetched && data ? : } + + + ); +}; diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx new file mode 100644 index 00000000000000..0e03da5d9adbd3 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { MetricTypes, isMetricType } from '../../../common/rest_types'; +import { useUrlParams } from '../../hooks/use_url_params'; +import { DEFAULT_DATE_RANGE_OPTIONS } from './use_date_picker'; + +interface UrlParamsDataUsageMetricsFilters { + metricTypes: string; + dataStreams: string; + startDate: string; + endDate: string; +} + +interface DataUsageMetricsFiltersFromUrlParams { + metricTypes?: MetricTypes[]; + dataStreams?: string[]; + startDate?: string; + endDate?: string; + setUrlDataStreamsFilter: (dataStreams: UrlParamsDataUsageMetricsFilters['dataStreams']) => void; + setUrlDateRangeFilter: ({ startDate, endDate }: { startDate: string; endDate: string }) => void; + setUrlMetricTypesFilter: (metricTypes: UrlParamsDataUsageMetricsFilters['metricTypes']) => void; +} + +type FiltersFromUrl = Pick< + DataUsageMetricsFiltersFromUrlParams, + 'metricTypes' | 'dataStreams' | 'startDate' | 'endDate' +>; + +export const getDataUsageMetricsFiltersFromUrlParams = ( + urlParams: Partial +): FiltersFromUrl => { + const dataUsageMetricsFilters: FiltersFromUrl = { + metricTypes: [], + dataStreams: [], + startDate: DEFAULT_DATE_RANGE_OPTIONS.startDate, + endDate: DEFAULT_DATE_RANGE_OPTIONS.endDate, + }; + + const urlMetricTypes = urlParams.metricTypes + ? String(urlParams.metricTypes) + .split(',') + .reduce((acc, curr) => { + if (isMetricType(curr)) { + acc.push(curr); + } + return acc.sort(); + }, []) + : []; + + const urlDataStreams = urlParams.dataStreams + ? String(urlParams.dataStreams).split(',').sort() + : []; + + dataUsageMetricsFilters.metricTypes = urlMetricTypes.length ? urlMetricTypes : undefined; + dataUsageMetricsFilters.dataStreams = urlDataStreams.length ? urlDataStreams : undefined; + dataUsageMetricsFilters.startDate = urlParams.startDate ? String(urlParams.startDate) : undefined; + dataUsageMetricsFilters.endDate = urlParams.endDate ? String(urlParams.endDate) : undefined; + + return dataUsageMetricsFilters; +}; + +export const useDataUsageMetricsUrlParams = (): DataUsageMetricsFiltersFromUrlParams => { + const location = useLocation(); + const history = useHistory(); + const { urlParams, toUrlParams } = useUrlParams(); + + const getUrlDataUsageMetricsFilters: FiltersFromUrl = useMemo( + () => getDataUsageMetricsFiltersFromUrlParams(urlParams), + [urlParams] + ); + const [dataUsageMetricsFilters, setDataUsageMetricsFilters] = useState( + getUrlDataUsageMetricsFilters + ); + + const setUrlMetricTypesFilter = useCallback( + (metricTypes: string) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + metricTypes: metricTypes.length ? metricTypes : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + const setUrlDataStreamsFilter = useCallback( + (dataStreams: string) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + dataStreams: dataStreams.length ? dataStreams : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + const setUrlDateRangeFilter = useCallback( + ({ startDate, endDate }: { startDate: string; endDate: string }) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + startDate: startDate.length ? startDate : undefined, + endDate: endDate.length ? endDate : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + useEffect(() => { + setDataUsageMetricsFilters((prevState) => { + return { + ...prevState, + ...getDataUsageMetricsFiltersFromUrlParams(urlParams), + }; + }); + }, [setDataUsageMetricsFilters, urlParams]); + + return { + ...dataUsageMetricsFilters, + setUrlDataStreamsFilter, + setUrlDateRangeFilter, + setUrlMetricTypesFilter, + }; +}; diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx new file mode 100644 index 00000000000000..b5407ae9e46d59 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/hooks/use_date_picker.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import type { + DurationRange, + OnRefreshChangeProps, +} from '@elastic/eui/src/components/date_picker/types'; +import { useDataUsageMetricsUrlParams } from './use_charts_url_params'; +import { DateRangePickerValues } from '../components/date_picker'; + +export const DEFAULT_DATE_RANGE_OPTIONS = Object.freeze({ + autoRefreshOptions: { + enabled: false, + duration: 10000, + }, + startDate: 'now-24h/h', + endDate: 'now', + recentlyUsedDateRanges: [], +}); + +export const useDateRangePicker = () => { + const { + setUrlDateRangeFilter, + startDate: startDateFromUrl, + endDate: endDateFromUrl, + } = useDataUsageMetricsUrlParams(); + const [dateRangePickerState, setDateRangePickerState] = useState({ + ...DEFAULT_DATE_RANGE_OPTIONS, + startDate: startDateFromUrl ?? DEFAULT_DATE_RANGE_OPTIONS.startDate, + endDate: endDateFromUrl ?? DEFAULT_DATE_RANGE_OPTIONS.endDate, + }); + + const updateUsageMetricsDateRanges = useCallback( + ({ start, end }: DurationRange) => { + setDateRangePickerState((prevState) => ({ + ...prevState, + startDate: start, + endDate: end, + })); + }, + [setDateRangePickerState] + ); + + const updateUsageMetricsRecentlyUsedDateRanges = useCallback( + (recentlyUsedDateRanges: DateRangePickerValues['recentlyUsedDateRanges']) => { + setDateRangePickerState((prevState) => ({ + ...prevState, + recentlyUsedDateRanges, + })); + }, + [setDateRangePickerState] + ); + + // handle refresh timer update + const onRefreshChange = useCallback( + (evt: OnRefreshChangeProps) => { + setDateRangePickerState((prevState) => ({ + ...prevState, + autoRefreshOptions: { enabled: !evt.isPaused, duration: evt.refreshInterval }, + })); + }, + [setDateRangePickerState] + ); + + // handle manual time change on date picker + const onTimeChange = useCallback( + ({ start: newStart, end: newEnd }: DurationRange) => { + // update date ranges + updateUsageMetricsDateRanges({ start: newStart, end: newEnd }); + + // update recently used date ranges + const newRecentlyUsedDateRanges = [ + { start: newStart, end: newEnd }, + ...dateRangePickerState.recentlyUsedDateRanges + .filter( + (recentlyUsedRange: DurationRange) => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + .slice(0, 9), + ]; + updateUsageMetricsRecentlyUsedDateRanges(newRecentlyUsedDateRanges); + setUrlDateRangeFilter({ startDate: newStart, endDate: newEnd }); + }, + [ + dateRangePickerState.recentlyUsedDateRanges, + setUrlDateRangeFilter, + updateUsageMetricsDateRanges, + updateUsageMetricsRecentlyUsedDateRanges, + ] + ); + + return { dateRangePickerState, onRefreshChange, onTimeChange }; +}; diff --git a/x-pack/plugins/data_usage/public/app/types.ts b/x-pack/plugins/data_usage/public/app/types.ts new file mode 100644 index 00000000000000..13f53bc2ea6dd8 --- /dev/null +++ b/x-pack/plugins/data_usage/public/app/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MetricTypes } from '../../common/rest_types'; + +export type DataPoint = [number, number]; // [timestamp, value] + +export interface MetricSeries { + name: string; // Name of the data stream + data: DataPoint[]; // Array of data points in tuple format [timestamp, value] +} +// Use MetricTypes dynamically as keys for the Metrics interface +export type Metrics = Partial>; + +export interface MetricsResponse { + metrics: Metrics; +} +export interface MetricsResponse { + metrics: Metrics; +} diff --git a/x-pack/plugins/data_usage/public/application.tsx b/x-pack/plugins/data_usage/public/application.tsx index 1e6c35c4b8f0a7..054aae397e5e15 100644 --- a/x-pack/plugins/data_usage/public/application.tsx +++ b/x-pack/plugins/data_usage/public/application.tsx @@ -16,6 +16,8 @@ import { PerformanceContextProvider } from '@kbn/ebt-tools'; import { useKibanaContextForPluginProvider } from './utils/use_kibana'; import { DataUsageStartDependencies, DataUsagePublicStart } from './types'; import { PLUGIN_ID } from '../common'; +import { DataUsage } from './app/data_usage'; +import { DataUsageReactQueryClientProvider } from '../common/query_client'; export const renderApp = ( core: CoreStart, @@ -51,7 +53,7 @@ const AppWithExecutionContext = ({ -
Data Usage
} /> +
@@ -76,7 +78,9 @@ const App = ({ core, plugins, pluginStart, params }: AppProps) => { return ( - + + + ); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts new file mode 100644 index 00000000000000..6b9860e997c125 --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -0,0 +1,45 @@ +/* + * 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 { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { + UsageMetricsRequestSchemaQueryParams, + UsageMetricsResponseSchemaBody, +} from '../../common/rest_types'; +import { DATA_USAGE_METRICS_API_ROUTE } from '../../common'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; + +interface ErrorType { + statusCode: number; + message: string; +} + +export const useGetDataUsageMetrics = ( + query: UsageMetricsRequestSchemaQueryParams, + options: UseQueryOptions> = {} +): UseQueryResult> => { + const http = useKibanaContextForPlugin().services.http; + + return useQuery>({ + queryKey: ['get-data-usage-metrics', query], + ...options, + keepPreviousData: true, + queryFn: async () => { + return http.get(DATA_USAGE_METRICS_API_ROUTE, { + version: '1', + query: { + from: query.from, + to: query.to, + metricTypes: query.metricTypes, + dataStreams: query.dataStreams, + }, + }); + }, + }); +}; diff --git a/x-pack/plugins/data_usage/public/hooks/use_url_params.ts b/x-pack/plugins/data_usage/public/hooks/use_url_params.ts new file mode 100644 index 00000000000000..865b71781df63f --- /dev/null +++ b/x-pack/plugins/data_usage/public/hooks/use_url_params.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 { useMemo } from 'react'; +import { parse, stringify } from 'query-string'; +import { useLocation } from 'react-router-dom'; + +/** + * Parses `search` params and returns an object with them along with a `toUrlParams` function + * that allows being able to retrieve a stringified version of an object (default is the + * `urlParams` that was parsed) for use in the url. + * Object will be recreated every time `search` changes. + */ +export function useUrlParams>(): { + urlParams: T; + toUrlParams: (params?: T) => string; +} { + const { search } = useLocation(); + return useMemo(() => { + const urlParams = parse(search) as unknown as T; + return { + urlParams, + toUrlParams: (params: T = urlParams) => stringify(params as unknown as object), + }; + }, [search]); +} diff --git a/x-pack/plugins/data_usage/public/utils/use_breadcrumbs.tsx b/x-pack/plugins/data_usage/public/utils/use_breadcrumbs.tsx new file mode 100644 index 00000000000000..928ee73ad52806 --- /dev/null +++ b/x-pack/plugins/data_usage/public/utils/use_breadcrumbs.tsx @@ -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 type { ChromeBreadcrumb, ChromeStart } from '@kbn/core-chrome-browser'; + +import { useEffect } from 'react'; +import { ManagementAppMountParams } from '@kbn/management-plugin/public'; + +export const useBreadcrumbs = ( + breadcrumbs: ChromeBreadcrumb[], + params: ManagementAppMountParams, + chromeService: ChromeStart +) => { + const { docTitle } = chromeService; + const isMultiple = breadcrumbs.length > 1; + + const docTitleValue = isMultiple ? breadcrumbs[breadcrumbs.length - 1].text : breadcrumbs[0].text; + + docTitle.change(docTitleValue as string); + + useEffect(() => { + params.setBreadcrumbs(breadcrumbs); + }, [breadcrumbs, params]); +}; diff --git a/x-pack/plugins/data_usage/server/common/errors.ts b/x-pack/plugins/data_usage/server/common/errors.ts new file mode 100644 index 00000000000000..7a43a10108be16 --- /dev/null +++ b/x-pack/plugins/data_usage/server/common/errors.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class BaseError extends Error { + constructor(message: string, public readonly meta?: MetaType) { + super(message); + // For debugging - capture name of subclasses + this.name = this.constructor.name; + + if (meta instanceof Error) { + this.stack += `\n----- original error -----\n${meta.stack}`; + } + } +} diff --git a/x-pack/plugins/data_usage/server/config.ts b/x-pack/plugins/data_usage/server/config.ts index 6453cce4f4d564..bf89431f2abea4 100644 --- a/x-pack/plugins/data_usage/server/config.ts +++ b/x-pack/plugins/data_usage/server/config.ts @@ -6,9 +6,18 @@ */ import { schema, type TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from '@kbn/core/server'; -export const config = schema.object({ +export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), }); -export type DataUsageConfig = TypeOf; +export type DataUsageConfigType = TypeOf; + +export const createConfig = (context: PluginInitializerContext): DataUsageConfigType => { + const pluginConfig = context.config.get>(); + + return { + ...pluginConfig, + }; +}; diff --git a/x-pack/plugins/data_usage/server/index.ts b/x-pack/plugins/data_usage/server/index.ts index 3aa49a184d0039..66d839303d7165 100644 --- a/x-pack/plugins/data_usage/server/index.ts +++ b/x-pack/plugins/data_usage/server/index.ts @@ -9,7 +9,7 @@ import type { PluginInitializerContext, PluginConfigDescriptor, } from '@kbn/core/server'; -import { DataUsageConfig } from './config'; +import { DataUsageConfigType } from './config'; import { DataUsagePlugin } from './plugin'; import type { @@ -19,11 +19,11 @@ import type { DataUsageStartDependencies, } from './types'; -import { config as configSchema } from './config'; +import { configSchema } from './config'; export type { DataUsageServerSetup, DataUsageServerStart }; -export const config: PluginConfigDescriptor = { +export const config: PluginConfigDescriptor = { schema: configSchema, }; @@ -32,5 +32,5 @@ export const plugin: PluginInitializer< DataUsageServerStart, DataUsageSetupDependencies, DataUsageStartDependencies -> = async (pluginInitializerContext: PluginInitializerContext) => +> = async (pluginInitializerContext: PluginInitializerContext) => await new DataUsagePlugin(pluginInitializerContext); diff --git a/x-pack/plugins/data_usage/server/plugin.ts b/x-pack/plugins/data_usage/server/plugin.ts index 8ab49d5104fffe..2beae9b22bba97 100644 --- a/x-pack/plugins/data_usage/server/plugin.ts +++ b/x-pack/plugins/data_usage/server/plugin.ts @@ -7,13 +7,17 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import { DataUsageConfig } from './config'; +import { DataUsageConfigType, createConfig } from './config'; import type { + DataUsageContext, + DataUsageRequestHandlerContext, DataUsageServerSetup, DataUsageServerStart, DataUsageSetupDependencies, DataUsageStartDependencies, } from './types'; +import { registerDataUsageRoutes } from './routes'; +import { PLUGIN_ID } from '../common'; export class DataUsagePlugin implements @@ -24,11 +28,39 @@ export class DataUsagePlugin DataUsageStartDependencies > { - logger: Logger; - constructor(context: PluginInitializerContext) { + private readonly logger: Logger; + private dataUsageContext: DataUsageContext; + + constructor(context: PluginInitializerContext) { + const serverConfig = createConfig(context); + this.logger = context.logger.get(); + + this.logger.debug('data usage plugin initialized'); + this.dataUsageContext = { + logFactory: context.logger, + get serverConfig() { + return serverConfig; + }, + }; } setup(coreSetup: CoreSetup, pluginsSetup: DataUsageSetupDependencies): DataUsageServerSetup { + this.logger.debug('data usage plugin setup'); + pluginsSetup.features.registerElasticsearchFeature({ + id: PLUGIN_ID, + management: { + data: [PLUGIN_ID], + }, + privileges: [ + { + requiredClusterPrivileges: ['monitor'], + ui: [], + }, + ], + }); + const router = coreSetup.http.createRouter(); + registerDataUsageRoutes(router, this.dataUsageContext); + return {}; } @@ -36,5 +68,7 @@ export class DataUsagePlugin return {}; } - public stop() {} + public stop() { + this.logger.debug('Stopping data usage plugin'); + } } diff --git a/x-pack/plugins/data_usage/server/routes/error_handler.ts b/x-pack/plugins/data_usage/server/routes/error_handler.ts new file mode 100644 index 00000000000000..122df5e72b130b --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/error_handler.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/server'; +import { CustomHttpRequestError } from '../utils/custom_http_request_error'; +import { BaseError } from '../common/errors'; + +export class NotFoundError extends BaseError {} + +/** + * Default Data Usage Routes error handler + * @param logger + * @param res + * @param error + */ +export const errorHandler = ( + logger: Logger, + res: KibanaResponseFactory, + error: E +): IKibanaResponse => { + logger.error(error); + + if (error instanceof CustomHttpRequestError) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + if (error instanceof NotFoundError) { + return res.notFound({ body: error }); + } + + // Kibana CORE will take care of `500` errors when the handler `throw`'s, including logging the error + throw error; +}; diff --git a/x-pack/plugins/data_usage/server/routes/index.tsx b/x-pack/plugins/data_usage/server/routes/index.tsx new file mode 100644 index 00000000000000..b6b80c38864f33 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataUsageContext, DataUsageRouter } from '../types'; +import { registerDataStreamsRoute, registerUsageMetricsRoute } from './internal'; + +export const registerDataUsageRoutes = ( + router: DataUsageRouter, + dataUsageContext: DataUsageContext +) => { + registerUsageMetricsRoute(router, dataUsageContext); + registerDataStreamsRoute(router, dataUsageContext); +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams.ts new file mode 100644 index 00000000000000..0d71d93b55849e --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStreamsResponseSchema } from '../../../common/rest_types'; +import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../../common'; +import { DataUsageContext, DataUsageRouter } from '../../types'; + +import { getDataStreamsHandler } from './data_streams_handler'; + +export const registerDataStreamsRoute = ( + router: DataUsageRouter, + dataUsageContext: DataUsageContext +) => { + if (dataUsageContext.serverConfig.enabled) { + router.versioned + .get({ + access: 'internal', + path: DATA_USAGE_DATA_STREAMS_API_ROUTE, + }) + .addVersion( + { + version: '1', + validate: { + request: {}, + response: { + 200: DataStreamsResponseSchema, + }, + }, + }, + getDataStreamsHandler(dataUsageContext) + ); + } +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts new file mode 100644 index 00000000000000..686edd0c4f4b7e --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.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. + */ + +import { RequestHandler } from '@kbn/core/server'; +import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types'; + +import { errorHandler } from '../error_handler'; + +export const getDataStreamsHandler = ( + dataUsageContext: DataUsageContext +): RequestHandler => { + const logger = dataUsageContext.logFactory.get('dataStreamsRoute'); + + return async (context, _, response) => { + logger.debug(`Retrieving user data streams`); + + try { + const core = await context.core; + const esClient = core.elasticsearch.client.asCurrentUser; + + const { data_streams: dataStreamsResponse } = await esClient.indices.dataStreamsStats({ + name: '*', + expand_wildcards: 'all', + }); + + const sorted = dataStreamsResponse + .sort((a, b) => b.store_size_bytes - a.store_size_bytes) + .map((dataStream) => ({ + name: dataStream.data_stream, + storageSizeBytes: dataStream.store_size_bytes, + })); + return response.ok({ + body: sorted, + }); + } catch (error) { + return errorHandler(logger, response, error); + } + }; +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/index.tsx b/x-pack/plugins/data_usage/server/routes/internal/index.tsx new file mode 100644 index 00000000000000..e8d874bb7e6af9 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/index.tsx @@ -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 { registerUsageMetricsRoute } from './usage_metrics'; +export { registerDataStreamsRoute } from './data_streams'; diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts new file mode 100644 index 00000000000000..5bf3008ef668a9 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UsageMetricsRequestSchema, UsageMetricsResponseSchema } from '../../../common/rest_types'; +import { DATA_USAGE_METRICS_API_ROUTE } from '../../../common'; +import { DataUsageContext, DataUsageRouter } from '../../types'; + +import { getUsageMetricsHandler } from './usage_metrics_handler'; + +export const registerUsageMetricsRoute = ( + router: DataUsageRouter, + dataUsageContext: DataUsageContext +) => { + if (dataUsageContext.serverConfig.enabled) { + router.versioned + .get({ + access: 'internal', + path: DATA_USAGE_METRICS_API_ROUTE, + }) + .addVersion( + { + version: '1', + validate: { + request: UsageMetricsRequestSchema, + response: { + 200: UsageMetricsResponseSchema, + }, + }, + }, + getUsageMetricsHandler(dataUsageContext) + ); + } +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts new file mode 100644 index 00000000000000..6f992c9fb2a383 --- /dev/null +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandler } from '@kbn/core/server'; +import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/types'; +import { + MetricTypes, + UsageMetricsRequestSchemaQueryParams, + UsageMetricsResponseSchema, +} from '../../../common/rest_types'; +import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types'; + +import { errorHandler } from '../error_handler'; + +const formatStringParams = (value: T | T[]): T[] | MetricTypes[] => + typeof value === 'string' ? [value] : value; + +export const getUsageMetricsHandler = ( + dataUsageContext: DataUsageContext +): RequestHandler< + never, + UsageMetricsRequestSchemaQueryParams, + unknown, + DataUsageRequestHandlerContext +> => { + const logger = dataUsageContext.logFactory.get('usageMetricsRoute'); + + return async (context, request, response) => { + try { + const core = await context.core; + const esClient = core.elasticsearch.client.asCurrentUser; + + // @ts-ignore + const { from, to, metricTypes, dataStreams: dsNames, size } = request.query; + logger.debug(`Retrieving usage metrics`); + + const { data_streams: dataStreamsResponse }: IndicesGetDataStreamResponse = + await esClient.indices.getDataStream({ + name: '*', + expand_wildcards: 'all', + }); + + const hasDataStreams = dataStreamsResponse.length > 0; + let userDsNames: string[] = []; + + if (dsNames?.length) { + userDsNames = typeof dsNames === 'string' ? [dsNames] : dsNames; + } else if (!userDsNames.length && hasDataStreams) { + userDsNames = dataStreamsResponse.map((ds) => ds.name); + } + + // If no data streams are found, return an empty response + if (!userDsNames.length) { + return response.ok({ + body: { + metrics: {}, + }, + }); + } + + const metrics = await fetchMetricsFromAutoOps({ + from, + to, + metricTypes: formatStringParams(metricTypes) as MetricTypes[], + dataStreams: formatStringParams(userDsNames), + }); + + return response.ok({ + body: { + metrics, + }, + }); + } catch (error) { + logger.error(`Error retrieving usage metrics: ${error.message}`); + return errorHandler(logger, response, error); + } + }; +}; + +const fetchMetricsFromAutoOps = async ({ + from, + to, + metricTypes, + dataStreams, +}: { + from: string; + to: string; + metricTypes: MetricTypes[]; + dataStreams: string[]; +}) => { + // TODO: fetch data from autoOps using userDsNames + /* + const response = await axios.post('https://api.auto-ops.{region}.{csp}.cloud.elastic.co/monitoring/serverless/v1/projects/{project_id}/metrics', { + from: Date.parse(from), + to: Date.parse(to), + metric_types: metricTypes, + allowed_indices: dataStreams, + }); + const { data } = response;*/ + // mock data from autoOps https://github.com/elastic/autoops-services/blob/master/monitoring/service/specs/serverless_project_metrics_api.yaml + const mockData = { + metrics: { + ingest_rate: [ + { + name: 'metrics-apache_spark.driver-default', + data: [ + [1726858530000, 13756849], + [1726862130000, 14657904], + [1726865730000, 12798561], + [1726869330000, 13578213], + [1726872930000, 14123495], + [1726876530000, 13876548], + [1726880130000, 12894561], + [1726883730000, 14478953], + [1726887330000, 14678905], + [1726890930000, 13976547], + [1726894530000, 14568945], + [1726898130000, 13789561], + [1726901730000, 14478905], + [1726905330000, 13956423], + [1726908930000, 14598234], + ], + }, + { + name: 'logs-apm.app.adservice-default', + data: [ + [1726858530000, 12894623], + [1726862130000, 14436905], + [1726865730000, 13794805], + [1726869330000, 14048532], + [1726872930000, 14237495], + [1726876530000, 13745689], + [1726880130000, 13974562], + [1726883730000, 14234653], + [1726887330000, 14323479], + [1726890930000, 14023945], + [1726894530000, 14189673], + [1726898130000, 14247895], + [1726901730000, 14098324], + [1726905330000, 14478905], + [1726908930000, 14323894], + ], + }, + { + name: 'metrics-apm.app.aws-lambdas-default', + data: [ + [1726858530000, 12576413], + [1726862130000, 13956423], + [1726865730000, 14568945], + [1726869330000, 14234856], + [1726872930000, 14368942], + [1726876530000, 13897654], + [1726880130000, 14456989], + [1726883730000, 14568956], + [1726887330000, 13987562], + [1726890930000, 14567894], + [1726894530000, 14246789], + [1726898130000, 14567895], + [1726901730000, 14457896], + [1726905330000, 14567895], + [1726908930000, 13989456], + ], + }, + ], + storage_retained: [ + { + name: 'metrics-apache_spark.driver-default', + data: [ + [1726858530000, 12576413], + [1726862130000, 13956423], + [1726865730000, 14568945], + [1726869330000, 14234856], + [1726872930000, 14368942], + [1726876530000, 13897654], + [1726880130000, 14456989], + [1726883730000, 14568956], + [1726887330000, 13987562], + [1726890930000, 14567894], + [1726894530000, 14246789], + [1726898130000, 14567895], + [1726901730000, 14457896], + [1726905330000, 14567895], + [1726908930000, 13989456], + ], + }, + { + name: 'logs-apm.app.adservice-default', + data: [ + [1726858530000, 12894623], + [1726862130000, 14436905], + [1726865730000, 13794805], + [1726869330000, 14048532], + [1726872930000, 14237495], + [1726876530000, 13745689], + [1726880130000, 13974562], + [1726883730000, 14234653], + [1726887330000, 14323479], + [1726890930000, 14023945], + [1726894530000, 14189673], + [1726898130000, 14247895], + [1726901730000, 14098324], + [1726905330000, 14478905], + [1726908930000, 14323894], + ], + }, + { + name: 'metrics-apm.app.aws-lambdas-default', + data: [ + [1726858530000, 12576413], + [1726862130000, 13956423], + [1726865730000, 14568945], + [1726869330000, 14234856], + [1726872930000, 14368942], + [1726876530000, 13897654], + [1726880130000, 14456989], + [1726883730000, 14568956], + [1726887330000, 13987562], + [1726890930000, 14567894], + [1726894530000, 14246789], + [1726898130000, 14567895], + [1726901730000, 14457896], + [1726905330000, 14567895], + [1726908930000, 13989456], + ], + }, + ], + }, + }; + // Make sure data is what we expect + const validatedData = UsageMetricsResponseSchema.body().validate(mockData); + + return validatedData.metrics; +}; diff --git a/x-pack/plugins/data_usage/server/types/index.ts b/x-pack/plugins/data_usage/server/types/index.ts new file mode 100644 index 00000000000000..6cc0ccaa93a6d1 --- /dev/null +++ b/x-pack/plugins/data_usage/server/types/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/data_usage/server/types/types.ts b/x-pack/plugins/data_usage/server/types/types.ts new file mode 100644 index 00000000000000..c90beb184f020f --- /dev/null +++ b/x-pack/plugins/data_usage/server/types/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CoreRequestHandlerContext, + CustomRequestHandlerContext, + IRouter, + LoggerFactory, +} from '@kbn/core/server'; +import { DeepReadonly } from 'utility-types'; +import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { DataUsageConfigType } from '../config'; + +export interface DataUsageSetupDependencies { + features: FeaturesPluginSetup; +} + +/* eslint-disable @typescript-eslint/no-empty-interface*/ +export interface DataUsageStartDependencies {} + +export interface DataUsageServerSetup {} + +export interface DataUsageServerStart {} + +interface DataUsageApiRequestHandlerContext { + core: CoreRequestHandlerContext; +} + +export type DataUsageRequestHandlerContext = CustomRequestHandlerContext<{ + dataUsage: DataUsageApiRequestHandlerContext; +}>; + +export type DataUsageRouter = IRouter; + +export interface DataUsageContext { + logFactory: LoggerFactory; + serverConfig: DeepReadonly; +} diff --git a/x-pack/plugins/data_usage/server/utils/custom_http_request_error.ts b/x-pack/plugins/data_usage/server/utils/custom_http_request_error.ts new file mode 100644 index 00000000000000..a7f00a0e82a3d0 --- /dev/null +++ b/x-pack/plugins/data_usage/server/utils/custom_http_request_error.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class CustomHttpRequestError extends Error { + constructor( + message: string, + public readonly statusCode: number = 500, + public readonly meta?: unknown + ) { + super(message); + // For debugging - capture name of subclasses + this.name = this.constructor.name; + + if (meta instanceof Error) { + this.stack += `\n----- original error -----\n${meta.stack}`; + } + } +} diff --git a/x-pack/plugins/data_usage/server/utils/index.ts b/x-pack/plugins/data_usage/server/utils/index.ts new file mode 100644 index 00000000000000..af46a18f61a799 --- /dev/null +++ b/x-pack/plugins/data_usage/server/utils/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 { CustomHttpRequestError } from './custom_http_request_error'; diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json index ebc023568cf887..cecbeb654db306 100644 --- a/x-pack/plugins/data_usage/tsconfig.json +++ b/x-pack/plugins/data_usage/tsconfig.json @@ -13,6 +13,7 @@ "kbn_references": [ "@kbn/core", "@kbn/i18n", + "@kbn/data-plugin", "@kbn/kibana-react-plugin", "@kbn/management-plugin", "@kbn/react-kibana-context-render", @@ -21,6 +22,12 @@ "@kbn/share-plugin", "@kbn/config-schema", "@kbn/logging", + "@kbn/deeplinks-observability", + "@kbn/unified-search-plugin", + "@kbn/i18n-react", + "@kbn/core-http-browser", + "@kbn/core-chrome-browser", + "@kbn/features-plugin", ], "exclude": ["target/**/*"] } From ca1d375348ef17721372612b408d9c31ac5d2494 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Tue, 8 Oct 2024 15:27:09 +0200 Subject: [PATCH 04/50] [Entity Analytics] Integration tests: Kibana spaces support in Entity Store (#194130) ### Summary This PR adds integration tests for Kibana spaces support in the Entity Store --------- Co-authored-by: Elastic Machine --- .../entity_store/entity_store_data_client.ts | 20 +- .../package.json | 9 +- .../trial_license_complete_tier/engine.ts | 89 ++----- .../engine_nondefault_spaces.ts | 234 ++++++++++++++++++ .../trial_license_complete_tier/index.ts | 1 + .../entity_analytics/utils/entity_store.ts | 102 ++++++-- 6 files changed, 362 insertions(+), 93 deletions(-) create mode 100644 x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index e99358bca1ac81..e40a18fbe480fe 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -68,7 +68,9 @@ export class EntityStoreDataClient { const definition = getEntityDefinition(entityType, this.options.namespace); - logger.info(`Initializing entity store for ${entityType}`); + logger.info( + `In namespace ${this.options.namespace}: Initializing entity store for ${entityType}` + ); const descriptor = await this.engineClient.init(entityType, definition, filter); await entityClient.createEntityDefinition({ @@ -92,11 +94,13 @@ export class EntityStoreDataClient { if (descriptor.status !== ENGINE_STATUS.STOPPED) { throw new Error( - `Cannot start Entity engine for ${entityType} when current status is: ${descriptor.status}` + `In namespace ${this.options.namespace}: Cannot start Entity engine for ${entityType} when current status is: ${descriptor.status}` ); } - this.options.logger.info(`Starting entity store for ${entityType}`); + this.options.logger.info( + `In namespace ${this.options.namespace}: Starting entity store for ${entityType}` + ); await this.options.entityClient.startEntityDefinition(definition); return this.engineClient.update(definition.id, ENGINE_STATUS.STARTED); @@ -109,11 +113,13 @@ export class EntityStoreDataClient { if (descriptor.status !== ENGINE_STATUS.STARTED) { throw new Error( - `Cannot stop Entity engine for ${entityType} when current status is: ${descriptor.status}` + `In namespace ${this.options.namespace}: Cannot stop Entity engine for ${entityType} when current status is: ${descriptor.status}` ); } - this.options.logger.info(`Stopping entity store for ${entityType}`); + this.options.logger.info( + `In namespace ${this.options.namespace}: Stopping entity store for ${entityType}` + ); await this.options.entityClient.stopEntityDefinition(definition); return this.engineClient.update(definition.id, ENGINE_STATUS.STOPPED); @@ -130,7 +136,9 @@ export class EntityStoreDataClient { public async delete(entityType: EntityType, deleteData: boolean) { const { id } = getEntityDefinition(entityType, this.options.namespace); - this.options.logger.info(`Deleting entity store for ${entityType}`); + this.options.logger.info( + `In namespace ${this.options.namespace}: Deleting entity store for ${entityType}` + ); await this.options.entityClient.deleteEntityDefinition({ id, deleteData }); await this.engineClient.delete(id); diff --git a/x-pack/test/security_solution_api_integration/package.json b/x-pack/test/security_solution_api_integration/package.json index 726d6b61feed53..1649c79f52a7d8 100644 --- a/x-pack/test/security_solution_api_integration/package.json +++ b/x-pack/test/security_solution_api_integration/package.json @@ -63,7 +63,7 @@ "nlp_cleanup_task:essentials:qa:serverless:release": "npm run run-tests:genai:basic_essentials nlp_cleanup_task serverless qaEnv", "entity_analytics:server:serverless": "npm run initialize-server:ea:trial_complete risk_engine serverless", - "entity_analytics:runner:serverless": "npm run run-tests:ea:trial_complete risk_engine serverless serverlessEnv", + "entity_analytics:runner:serverless": "npm run run-tests:ea:trial_complete risk_engine serverless serverlessEnv --", "entity_analytics:qa:serverless": "npm run run-tests:ea:trial_complete risk_engine serverless qaPeriodicEnv", "entity_analytics:qa:serverless:release": "npm run run-tests:ea:trial_complete risk_engine serverless qaEnv", "entity_analytics:server:ess": "npm run initialize-server:ea:trial_complete risk_engine ess", @@ -76,6 +76,13 @@ "entity_analytics:essentials:server:ess": "npm run initialize-server:ea:basic_essentials risk_engine ess", "entity_analytics:essentials:runner:ess": "npm run run-tests:ea:basic_essentials risk_engine ess essEnv", + "entity_analytics:entity_store:server:serverless": "npm run initialize-server:ea:trial_complete entity_store serverless", + "entity_analytics:entity_store:runner:serverless": "npm run run-tests:ea:trial_complete entity_store serverless serverlessEnv", + "entity_analytics:entity_store:qa:serverless": "npm run run-tests:ea:trial_complete entity_store serverless qaPeriodicEnv", + "entity_analytics:entity_store:qa:serverless:release": "npm run run-tests:ea:trial_complete entity_store serverless qaEnv", + "entity_analytics:entity_store:server:ess": "npm run initialize-server:ea:trial_complete entity_store ess", + "entity_analytics:entity_store:runner:ess": "npm run run-tests:ea:trial_complete entity_store ess essEnv --", + "edr_workflows:artifacts:server:serverless": "npm run initialize-server:edr-workflows artifacts serverless", "edr_workflows:artifacts:runner:serverless": "npm run run-tests:edr-workflows artifacts serverless serverlessEnv", "edr_workflows:artifacts:qa:serverless": "npm run run-tests:edr-workflows artifacts serverless qaPeriodicEnv", diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts index 8d57ff428e5077..99d84fbc5427bf 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts @@ -6,104 +6,57 @@ */ import expect from '@kbn/expect'; -import { EntityType } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/common.gen'; + import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { cleanEngines } from '../../utils'; +import { EntityStoreUtils } from '../../utils'; export default ({ getService }: FtrProviderContext) => { const api = getService('securitySolutionApi'); - const es = getService('es'); - - const initEntityEngineForEntityType = async (entityType: EntityType) => { - return api - .initEntityEngine({ - params: { entityType }, - body: {}, - }) - .expect(200); - }; - - const expectTransformExists = async (transformId: string) => { - return expectTransformStatus(transformId, true); - }; - - const expectTransformNotFound = async (transformId: string, attempts: number = 5) => { - return expectTransformStatus(transformId, false); - }; - - const expectTransformStatus = async ( - transformId: string, - exists: boolean, - attempts: number = 5, - delayMs: number = 2000 - ) => { - let currentAttempt = 1; - while (currentAttempt <= attempts) { - try { - await es.transform.getTransform({ transform_id: transformId }); - if (!exists) { - throw new Error(`Expected transform ${transformId} to not exist, but it does`); - } - return; // Transform exists, exit the loop - } catch (e) { - if (currentAttempt === attempts) { - if (exists) { - throw new Error(`Expected transform ${transformId} to exist, but it does not: ${e}`); - } else { - return; // Transform does not exist, exit the loop - } - } - await new Promise((resolve) => setTimeout(resolve, delayMs)); - currentAttempt++; - } - } - }; - - const expectTransformsExist = async (transformIds: string[]) => - Promise.all(transformIds.map((id) => expectTransformExists(id))); + + const utils = EntityStoreUtils(getService); describe('@ess @serverless @skipInServerlessMKI Entity Store Engine APIs', () => { before(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); describe('init', () => { afterEach(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); it('should have installed the expected user resources', async () => { - await initEntityEngineForEntityType('user'); + await utils.initEntityEngineForEntityType('user'); const expectedTransforms = [ 'entities-v1-history-ea_default_user_entity_store', 'entities-v1-latest-ea_default_user_entity_store', ]; - await expectTransformsExist(expectedTransforms); + await utils.expectTransformsExist(expectedTransforms); }); it('should have installed the expected host resources', async () => { - await initEntityEngineForEntityType('host'); + await utils.initEntityEngineForEntityType('host'); const expectedTransforms = [ 'entities-v1-history-ea_default_host_entity_store', 'entities-v1-latest-ea_default_host_entity_store', ]; - await expectTransformsExist(expectedTransforms); + await utils.expectTransformsExist(expectedTransforms); }); }); describe('get and list', () => { before(async () => { await Promise.all([ - initEntityEngineForEntityType('host'), - initEntityEngineForEntityType('user'), + utils.initEntityEngineForEntityType('host'), + utils.initEntityEngineForEntityType('user'), ]); }); after(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); describe('get', () => { @@ -169,11 +122,11 @@ export default ({ getService }: FtrProviderContext) => { describe('start and stop', () => { before(async () => { - await initEntityEngineForEntityType('host'); + await utils.initEntityEngineForEntityType('host'); }); after(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); it('should stop the entity engine', async () => { @@ -211,7 +164,7 @@ export default ({ getService }: FtrProviderContext) => { describe('delete', () => { it('should delete the host entity engine', async () => { - await initEntityEngineForEntityType('host'); + await utils.initEntityEngineForEntityType('host'); await api .deleteEntityEngine({ @@ -220,12 +173,12 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-history-ea_host_entity_store'); - await expectTransformNotFound('entities-v1-latest-ea_host_entity_store'); + await utils.expectTransformNotFound('entities-v1-history-ea_host_entity_store'); + await utils.expectTransformNotFound('entities-v1-latest-ea_host_entity_store'); }); it('should delete the user entity engine', async () => { - await initEntityEngineForEntityType('user'); + await utils.initEntityEngineForEntityType('user'); await api .deleteEntityEngine({ @@ -234,8 +187,8 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-history-ea_user_entity_store'); - await expectTransformNotFound('entities-v1-latest-ea_user_entity_store'); + await utils.expectTransformNotFound('entities-v1-history-ea_user_entity_store'); + await utils.expectTransformNotFound('entities-v1-latest-ea_user_entity_store'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts new file mode 100644 index 00000000000000..112c8b8b215114 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { v4 as uuidv4 } from 'uuid'; +import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces'; +import { EntityStoreUtils } from '../../utils'; +export default ({ getService }: FtrProviderContextWithSpaces) => { + const api = getService('securitySolutionApi'); + const spaces = getService('spaces'); + const namespace = uuidv4().substring(0, 8); + + const utils = EntityStoreUtils(getService, namespace); + + describe('@ess Entity Store Engine APIs in non-default space', () => { + before(async () => { + await utils.cleanEngines(); + await spaces.create({ + id: namespace, + name: namespace, + disabledFeatures: [], + }); + }); + + after(async () => { + await spaces.delete(namespace); + }); + + describe('init', () => { + afterEach(async () => { + await utils.cleanEngines(); + }); + + it('should have installed the expected user resources', async () => { + await utils.initEntityEngineForEntityType('user'); + + const expectedTransforms = [ + `entities-v1-history-ea_${namespace}_user_entity_store`, + `entities-v1-latest-ea_${namespace}_user_entity_store`, + ]; + + await utils.expectTransformsExist(expectedTransforms); + }); + + it('should have installed the expected host resources', async () => { + await utils.initEntityEngineForEntityType('host'); + + const expectedTransforms = [ + `entities-v1-history-ea_${namespace}_host_entity_store`, + `entities-v1-latest-ea_${namespace}_host_entity_store`, + ]; + + await utils.expectTransformsExist(expectedTransforms); + }); + }); + + describe('get and list', () => { + before(async () => { + await Promise.all([ + utils.initEntityEngineForEntityType('host'), + utils.initEntityEngineForEntityType('user'), + ]); + }); + + after(async () => { + await utils.cleanEngines(); + }); + + describe('get', () => { + it('should return the host entity engine', async () => { + const getResponse = await api + .getEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + expect(getResponse.body).to.eql({ + status: 'started', + type: 'host', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }); + }); + + it('should return the user entity engine', async () => { + const getResponse = await api + .getEntityEngine( + { + params: { entityType: 'user' }, + }, + namespace + ) + .expect(200); + + expect(getResponse.body).to.eql({ + status: 'started', + type: 'user', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }); + }); + }); + + describe('list', () => { + it('should return the list of entity engines', async () => { + const { body } = await api.listEntityEngines(namespace).expect(200); + + // @ts-expect-error body is any + const sortedEngines = body.engines.sort((a, b) => a.type.localeCompare(b.type)); + + expect(sortedEngines).to.eql([ + { + status: 'started', + type: 'host', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }, + { + status: 'started', + type: 'user', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }, + ]); + }); + }); + }); + + describe('start and stop', () => { + before(async () => { + await utils.initEntityEngineForEntityType('host'); + }); + + after(async () => { + await utils.cleanEngines(); + }); + + it('should stop the entity engine', async () => { + await api + .stopEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + const { body } = await api + .getEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + expect(body.status).to.eql('stopped'); + }); + + it('should start the entity engine', async () => { + await api + .startEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + const { body } = await api + .getEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + expect(body.status).to.eql('started'); + }); + }); + + describe('delete', () => { + it('should delete the host entity engine', async () => { + await utils.initEntityEngineForEntityType('host'); + + await api + .deleteEntityEngine( + { + params: { entityType: 'host' }, + query: { data: true }, + }, + namespace + ) + .expect(200); + + await utils.expectTransformNotFound( + `entities-v1-history-ea_${namespace}_host_entity_store` + ); + await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_host_entity_store`); + }); + + it('should delete the user entity engine', async () => { + await utils.initEntityEngineForEntityType('user'); + + await api + .deleteEntityEngine( + { + params: { entityType: 'user' }, + query: { data: true }, + }, + namespace + ) + .expect(200); + + await utils.expectTransformNotFound( + `entities-v1-history-ea_${namespace}_user_entity_store` + ); + await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_user_entity_store`); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts index 6e730b465350aa..ec9c7a8f1dcbab 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Entity Analytics - Entity Store', function () { loadTestFile(require.resolve('./entities_list')); loadTestFile(require.resolve('./engine')); + loadTestFile(require.resolve('./engine_nondefault_spaces')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts index 3f0ba0698a4942..9dc1807d7263c6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts @@ -5,29 +5,95 @@ * 2.0. */ +import { EntityType } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/common.gen'; + import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; -export const cleanEngines = async ({ - getService, -}: { - getService: FtrProviderContext['getService']; -}) => { - const log = getService('log'); +export const EntityStoreUtils = ( + getService: FtrProviderContext['getService'], + namespace?: string +) => { const api = getService('securitySolutionApi'); + const es = getService('es'); + const log = getService('log'); + + log.debug(`EntityStoreUtils namespace: ${namespace}`); + + const cleanEngines = async () => { + const { body } = await api.listEntityEngines(namespace).expect(200); - const { body } = await api.listEntityEngines().expect(200); + // @ts-expect-error body is any + const engineTypes = body.engines.map((engine) => engine.type); - // @ts-expect-error body is any - const engineTypes = body.engines.map((engine) => engine.type); + log.info(`Cleaning engines: ${engineTypes.join(', ')}`); + try { + await Promise.all( + engineTypes.map((entityType: 'user' | 'host') => + api.deleteEntityEngine({ params: { entityType }, query: { data: true } }, namespace) + ) + ); + } catch (e) { + log.warning(`Error deleting engines: ${e.message}`); + } + }; - log.info(`Cleaning engines: ${engineTypes.join(', ')}`); - try { - await Promise.all( - engineTypes.map((entityType: 'user' | 'host') => - api.deleteEntityEngine({ params: { entityType }, query: { data: true } }) + const initEntityEngineForEntityType = async (entityType: EntityType) => { + log.info(`Initializing engine for entity type ${entityType} in namespace ${namespace}`); + return api + .initEntityEngine( + { + params: { entityType }, + body: {}, + }, + namespace ) - ); - } catch (e) { - log.warning(`Error deleting engines: ${e.message}`); - } + .expect(200); + }; + + const expectTransformStatus = async ( + transformId: string, + exists: boolean, + attempts: number = 5, + delayMs: number = 2000 + ) => { + let currentAttempt = 1; + while (currentAttempt <= attempts) { + try { + await es.transform.getTransform({ transform_id: transformId }); + if (!exists) { + throw new Error(`Expected transform ${transformId} to not exist, but it does`); + } + return; // Transform exists, exit the loop + } catch (e) { + if (currentAttempt === attempts) { + if (exists) { + throw new Error(`Expected transform ${transformId} to exist, but it does not: ${e}`); + } else { + return; // Transform does not exist, exit the loop + } + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + currentAttempt++; + } + } + }; + + const expectTransformNotFound = async (transformId: string, attempts: number = 5) => { + return expectTransformStatus(transformId, false); + }; + const expectTransformExists = async (transformId: string) => { + return expectTransformStatus(transformId, true); + }; + + const expectTransformsExist = async (transformIds: string[]) => + Promise.all(transformIds.map((id) => expectTransformExists(id))); + + return { + cleanEngines, + initEntityEngineForEntityType, + expectTransformStatus, + expectTransformNotFound, + expectTransformExists, + expectTransformsExist, + }; }; From ef4755a063e5e08af3ebd44c2a866ac804eabcd4 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Tue, 8 Oct 2024 15:34:27 +0200 Subject: [PATCH 05/50] [EDR Workflows] Deprecate public endpoint/suggestions api endpoint in favour of an internal one (#194832) New internal GET `/internal/api/endpoint/suggestions/{suggestion_type}` route. Current public GET `/api/endpoint/suggestions/{suggestion_type}` route is set to deprecated. UI uses now the internal GET `/internal/api/endpoint/suggestions/{suggestion_type}` api route --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../output/kibana.serverless.staging.yaml | 1 + oas_docs/output/kibana.serverless.yaml | 1 + oas_docs/output/kibana.staging.yaml | 1 + oas_docs/output/kibana.yaml | 1 + .../suggestions/get_suggestions.schema.yaml | 1 + .../common/endpoint/constants.ts | 3 ++ ...agement_api_2023_10_31.bundled.schema.yaml | 1 + ...agement_api_2023_10_31.bundled.schema.yaml | 1 + .../event_filters/service/api_client.test.ts | 44 +++++++++++++++++++ .../pages/event_filters/service/api_client.ts | 6 +-- .../exceptions_list_api_client.test.ts | 4 +- .../endpoint/routes/suggestions/index.test.ts | 9 ++-- .../endpoint/routes/suggestions/index.ts | 27 +++++++++++- 13 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.test.ts diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 8bd9bd198e5e1d..66790ef3f9c178 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -8096,6 +8096,7 @@ paths: - Security Endpoint Management API /api/endpoint/suggestions/{suggestion_type}: post: + deprecated: true operationId: GetEndpointSuggestions parameters: - in: path diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 8bd9bd198e5e1d..66790ef3f9c178 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -8096,6 +8096,7 @@ paths: - Security Endpoint Management API /api/endpoint/suggestions/{suggestion_type}: post: + deprecated: true operationId: GetEndpointSuggestions parameters: - in: path diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 1ceeae519da724..7dbb9571f02720 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -11468,6 +11468,7 @@ paths: - Security Endpoint Management API /api/endpoint/suggestions/{suggestion_type}: post: + deprecated: true operationId: GetEndpointSuggestions parameters: - in: path diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 1ceeae519da724..7dbb9571f02720 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -11468,6 +11468,7 @@ paths: - Security Endpoint Management API /api/endpoint/suggestions/{suggestion_type}: post: + deprecated: true operationId: GetEndpointSuggestions parameters: - in: path diff --git a/x-pack/plugins/security_solution/common/api/endpoint/suggestions/get_suggestions.schema.yaml b/x-pack/plugins/security_solution/common/api/endpoint/suggestions/get_suggestions.schema.yaml index 573f9c0e3992f3..08006d91045ff4 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/suggestions/get_suggestions.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/endpoint/suggestions/get_suggestions.schema.yaml @@ -5,6 +5,7 @@ info: paths: /api/endpoint/suggestions/{suggestion_type}: post: + deprecated: true summary: Get suggestions operationId: GetEndpointSuggestions x-codegen-enabled: true diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 276d2fa32da76e..0e7218f0d7d413 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -60,6 +60,7 @@ export const FILE_STORAGE_DATA_INDEX = getFileDataIndexName('endpoint'); // Location from where all Endpoint related APIs are mounted export const BASE_ENDPOINT_ROUTE = '/api/endpoint'; +export const BASE_INTERNAL_ENDPOINT_ROUTE = `/internal${BASE_ENDPOINT_ROUTE}`; // Endpoint API routes export const HOST_METADATA_LIST_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata`; @@ -72,7 +73,9 @@ export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`; export const PROTECTION_UPDATES_NOTE_ROUTE = `${BASE_ENDPOINT_ROUTE}/protection_updates_note/{package_policy_id}`; /** Suggestions routes */ +/** @deprecated public route, use {@link SUGGESTIONS_INTERNAL_ROUTE} internal route */ export const SUGGESTIONS_ROUTE = `${BASE_ENDPOINT_ROUTE}/suggestions/{suggestion_type}`; +export const SUGGESTIONS_INTERNAL_ROUTE = `${BASE_INTERNAL_ENDPOINT_ROUTE}/suggestions/{suggestion_type}`; /** * Action Response Routes diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index ae35a302cbb42b..5d03e10969e14d 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -545,6 +545,7 @@ paths: - Security Endpoint Management API /api/endpoint/suggestions/{suggestion_type}: post: + deprecated: true operationId: GetEndpointSuggestions parameters: - in: path diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml index df98520fe783f3..f5c2d290af33c3 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -495,6 +495,7 @@ paths: - Security Endpoint Management API /api/endpoint/suggestions/{suggestion_type}: post: + deprecated: true operationId: GetEndpointSuggestions parameters: - in: path diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.test.ts new file mode 100644 index 00000000000000..9810bf6d290605 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core-http-browser'; +import { EventFiltersApiClient } from './api_client'; +import { coreMock } from '@kbn/core/public/mocks'; +import { SUGGESTIONS_INTERNAL_ROUTE } from '../../../../../common/endpoint/constants'; +import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; + +describe('EventFiltersApiClient', () => { + let fakeHttpServices: jest.Mocked; + let eventFiltersApiClient: EventFiltersApiClient; + + beforeAll(() => { + fakeHttpServices = coreMock.createStart().http as jest.Mocked; + eventFiltersApiClient = new EventFiltersApiClient(fakeHttpServices); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call the SUGGESTIONS_INTERNAL_ROUTE with correct URL and body', async () => { + await eventFiltersApiClient.getSuggestions({ + field: 'host.name', + query: 'test', + }); + + expect(fakeHttpServices.post).toHaveBeenCalledWith( + resolvePathVariables(SUGGESTIONS_INTERNAL_ROUTE, { suggestion_type: 'eventFilters' }), + { + version: '1', + body: JSON.stringify({ + field: 'host.name', + query: 'test', + }), + } + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts index e7c5e53e34274c..48bfcb9d848d1f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/api_client.ts @@ -12,8 +12,8 @@ import type { UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { removeIdFromExceptionItemsEntries } from '@kbn/securitysolution-list-hooks'; +import { SUGGESTIONS_INTERNAL_ROUTE } from '../../../../../common/endpoint/constants'; import type { EndpointSuggestionsBody } from '../../../../../common/api/endpoint'; -import { SUGGESTIONS_ROUTE } from '../../../../../common/endpoint/constants'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; import { EVENT_FILTER_LIST_DEFINITION } from '../constants'; @@ -55,9 +55,9 @@ export class EventFiltersApiClient extends ExceptionsListApiClient { */ async getSuggestions(body: EndpointSuggestionsBody): Promise { const result: string[] = await this.getHttp().post( - resolvePathVariables(SUGGESTIONS_ROUTE, { suggestion_type: 'eventFilters' }), + resolvePathVariables(SUGGESTIONS_INTERNAL_ROUTE, { suggestion_type: 'eventFilters' }), { - version: this.version, + version: '1', body: JSON.stringify(body), } ); diff --git a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts index 213b8da54dcd00..0424ff8ce6db34 100644 --- a/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts +++ b/x-pack/plugins/security_solution/public/management/services/exceptions_list/exceptions_list_api_client.test.ts @@ -59,9 +59,9 @@ describe('Exceptions List Api Client', () => { ); }); - describe('Wen getting an instance', () => { + describe('When getting an instance', () => { /** - * ATENTION: Skipping or modifying this test may cause the other test fails because it's creating the initial Singleton instance. + * ATTENTION: Skipping or modifying this test may cause the other test fails because it's creating the initial Singleton instance. * If you want to run tests individually, add this one to the execution with the .only method */ it('New instance is created the first time and the create list method is called', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts index d8df4fc7131e06..e122367303340d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.test.ts @@ -39,7 +39,10 @@ import { } from '.'; import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks'; -import { eventsIndexPattern, SUGGESTIONS_ROUTE } from '../../../../common/endpoint/constants'; +import { + eventsIndexPattern, + SUGGESTIONS_INTERNAL_ROUTE, +} from '../../../../common/endpoint/constants'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; jest.mock('@kbn/unified-search-plugin/server/autocomplete/terms_enum', () => { @@ -184,7 +187,7 @@ describe('when calling the Suggestions route handler', () => { routerMock, 'post', routePrefix, - '2023-10-31' + '1' ); await routeHandler(ctx as unknown as RequestHandlerContext, mockRequest, mockResponse); @@ -192,7 +195,7 @@ describe('when calling the Suggestions route handler', () => { }); it('should respond with forbidden', async () => { - await callRoute(SUGGESTIONS_ROUTE, { + await callRoute(SUGGESTIONS_INTERNAL_ROUTE, { params: { suggestion_type: 'eventFilters' }, authz: { canReadEventFilters: true, canWriteEventFilters: false }, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts index 6e84e101d25062..a4853d9772ad7b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts @@ -21,7 +21,11 @@ import type { SecuritySolutionRequestHandlerContext, } from '../../../types'; import type { EndpointAppContext } from '../../types'; -import { eventsIndexPattern, SUGGESTIONS_ROUTE } from '../../../../common/endpoint/constants'; +import { + eventsIndexPattern, + SUGGESTIONS_INTERNAL_ROUTE, + SUGGESTIONS_ROUTE, +} from '../../../../common/endpoint/constants'; import { withEndpointAuthz } from '../with_endpoint_authz'; import { errorHandler } from '../error_handler'; @@ -39,6 +43,7 @@ export function registerEndpointSuggestionsRoutes( access: 'public', path: SUGGESTIONS_ROUTE, options: { authRequired: true, tags: ['access:securitySolution'] }, + deprecated: true, }) .addVersion( { @@ -53,6 +58,26 @@ export function registerEndpointSuggestionsRoutes( getEndpointSuggestionsRequestHandler(config$, getLogger(endpointContext)) ) ); + + router.versioned + .post({ + access: 'internal', + path: SUGGESTIONS_INTERNAL_ROUTE, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }) + .addVersion( + { + version: '1', + validate: { + request: EndpointSuggestionsSchema, + }, + }, + withEndpointAuthz( + { any: ['canWriteEventFilters'] }, + endpointContext.logFactory.get('endpointSuggestions'), + getEndpointSuggestionsRequestHandler(config$, getLogger(endpointContext)) + ) + ); } export const getEndpointSuggestionsRequestHandler = ( From 8281517ef5ede44519dbb52abaf59d4f86d9396e Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 8 Oct 2024 10:21:47 -0400 Subject: [PATCH 06/50] chore(slo): add access:public missing options for public routes (#195114) --- .../observability_solution/slo/server/routes/slo/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts index 703c8aab5a684a..838adc72cfd08c 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/slo/route.ts @@ -516,6 +516,7 @@ const deleteSloInstancesRoute = createSloServerRoute({ endpoint: 'POST /api/observability/slos/_delete_instances 2023-10-31', options: { tags: ['access:slo_write'], + access: 'public', }, params: deleteSLOInstancesParamsSchema, handler: async ({ context, params }) => { @@ -532,6 +533,7 @@ const findSloDefinitionsRoute = createSloServerRoute({ endpoint: 'GET /api/observability/slos/_definitions 2023-10-31', options: { tags: ['access:slo_read'], + access: 'public', }, params: findSloDefinitionsParamsSchema, handler: async ({ context, params, logger }) => { From d20c579304b5fd212971bd9ef80cfbbda0932d94 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:22:09 +0100 Subject: [PATCH 07/50] [SecuritySolution] Allow custom sorting on integration cards (#195397) Part of https://github.com/elastic/kibana/pull/193131 It shows the customised cards without applying the default sorting under the `recommended` tab: Added two optional props: 1. `calloutTopSpacerSize` - Props to decide the size of the spacer above callout. Security Solution uses this prop to customize the size of the spacer 2. `sortByFeaturedIntegrations` - Customizing whether to sort by the default featured integrations' categories. Security Solution has `custom sorting logic` Featured cards: 1. AWS 3. GCP 4. Azure 5. Elastic Defend 6. CrowdStrike (to promote our extended protections/3rd party EDR support) 7. Wiz (or another cloud integration to promote extended protections) 8. Network Packet Capture 9. Osquery Manager 10. ~**Cloud Asset Inventory (need to confirm that's confirmed for 8.16)** - Currently Not Found any integration matched~ Screenshot 2024-10-08 at 13 31 12 --- .../epm/components/package_list_grid/grid.tsx | 1 - .../epm/components/package_list_grid/index.tsx | 15 +++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx index 959b5682792b99..3d7b5348607a0c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx @@ -68,7 +68,6 @@ export const GridColumn = ({ }: GridColumnProps) => { const itemsSizeRefs = useRef(new Map()); const listRef = useRef(null); - const onHeightChange = useCallback((index: number, size: number) => { itemsSizeRefs.current.set(index, size); if (listRef.current) { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx index be2b873c317db4..ba90ed6509f95d 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx @@ -61,6 +61,8 @@ export interface PackageListGridProps { setUrlandReplaceHistory: (params: IntegrationsURLParameters) => void; setUrlandPushHistory: (params: IntegrationsURLParameters) => void; callout?: JSX.Element | null; + // Props to decide the size of the spacer above callout. Security Solution uses this prop to customize the size of the spacer + calloutTopSpacerSize?: 's' | 'm' | 'xs' | 'l' | 'xl' | 'xxl'; // Props used only in AvailablePackages component: showCardLabels?: boolean; title?: string; @@ -70,6 +72,8 @@ export interface PackageListGridProps { showMissingIntegrationMessage?: boolean; showControls?: boolean; showSearchTools?: boolean; + // Customizing whether to sort by the default featured integrations' categories. Security Solution has custom sorting logic + sortByFeaturedIntegrations?: boolean; spacer?: boolean; // Security Solution sends the id to determine which element to scroll when the user interacting with the package list scrollElementId?: string; @@ -92,7 +96,9 @@ export const PackageListGrid: FunctionComponent = ({ setUrlandReplaceHistory, setUrlandPushHistory, showMissingIntegrationMessage = false, + sortByFeaturedIntegrations = true, callout, + calloutTopSpacerSize = 'l', // Default EUI spacer size showCardLabels = true, showControls = true, showSearchTools = true, @@ -141,9 +147,10 @@ export const PackageListGrid: FunctionComponent = ({ ) : list; - return promoteFeaturedIntegrations(filteredList, selectedCategory); - }, [isLoading, list, localSearchRef, searchTerm, selectedCategory]); - + return sortByFeaturedIntegrations + ? promoteFeaturedIntegrations(filteredList, selectedCategory) + : filteredList; + }, [isLoading, list, localSearchRef, searchTerm, selectedCategory, sortByFeaturedIntegrations]); const splitSubcategories = ( subcategories: CategoryFacet[] | undefined ): { visibleSubCategories?: CategoryFacet[]; hiddenSubCategories?: CategoryFacet[] } => { @@ -270,7 +277,7 @@ export const PackageListGrid: FunctionComponent = ({ ) : null} {callout ? ( <> - + {callout} ) : null} From d573915dd30c62adb780728ab83668285f5bd64d Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Tue, 8 Oct 2024 09:29:59 -0500 Subject: [PATCH 08/50] [Search][Onboarding] Default home to Global Empty State (#195142) ## Summary Updated the ES3 (Serverless Search) default home route to be the global empty state, when `search_indices` is enabled. Moved the getting started page, the current homepage, from `/app/elasticsearch` to `/app/elasticsearch/getting_started` This required adding a redirect for `/app/elasticsearch` to `/app/elasticsearch/start`. After we enabled `search_indices` by default for ES3, we can remove the conditional logic added by this PR. ### Screenshots ES3 Home With search indices config FF enabled ![image](https://github.com/user-attachments/assets/9a2227c0-8ec3-4e98-ba5c-08cebf8d3df4) ES3 Home with search indices config FF disabled ![image](https://github.com/user-attachments/assets/68bb6f36-f754-4f6b-9637-cf419ef21945) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --- packages/deeplinks/search/constants.ts | 2 + packages/deeplinks/search/deep_links.ts | 8 +- x-pack/plugins/search_indices/common/index.ts | 4 + .../public/components/start/create_index.tsx | 33 ++++---- .../components/start/elasticsearch_start.tsx | 2 - .../start/hooks/use_indices_redirect.tsx | 25 +++++- .../plugins/search_indices/public/plugin.ts | 18 ++-- .../plugins/search_indices/public/routes.ts | 3 + x-pack/plugins/search_indices/public/types.ts | 13 ++- x-pack/plugins/search_indices/tsconfig.json | 4 +- .../public/navigation_tree.ts | 33 +++++++- .../serverless_search/public/plugin.ts | 37 +++++++-- .../plugins/serverless_search/public/types.ts | 6 ++ .../plugins/serverless_search/tsconfig.json | 1 + .../test_serverless/functional/config.base.ts | 1 + .../services/svl_search_navigation.ts | 14 +++- .../search/config.feature_flags.ts | 11 +++ .../functional/test_suites/search/config.ts | 11 +++ .../test_suites/search/elasticsearch_start.ts | 82 ++++++++++--------- .../{landing_page.ts => getting_started.ts} | 8 +- .../test_suites/search/index.feature_flags.ts | 1 + .../functional/test_suites/search/index.ts | 2 +- .../test_suites/search/search_index_detail.ts | 3 +- x-pack/test_serverless/shared/types/index.ts | 1 + 24 files changed, 237 insertions(+), 86 deletions(-) rename x-pack/test_serverless/functional/test_suites/search/{landing_page.ts => getting_started.ts} (95%) diff --git a/packages/deeplinks/search/constants.ts b/packages/deeplinks/search/constants.ts index 76a9834d061e12..c4a598145c87c6 100644 --- a/packages/deeplinks/search/constants.ts +++ b/packages/deeplinks/search/constants.ts @@ -19,3 +19,5 @@ export const SERVERLESS_ES_CONNECTORS_ID = 'serverlessConnectors'; export const SERVERLESS_ES_SEARCH_PLAYGROUND_ID = 'searchPlayground'; export const SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID = 'searchInferenceEndpoints'; export const SEARCH_HOMEPAGE = 'searchHomepage'; +export const SEARCH_INDICES_START = 'elasticsearchStart'; +export const SEARCH_INDICES = 'elasticsearchIndices'; diff --git a/packages/deeplinks/search/deep_links.ts b/packages/deeplinks/search/deep_links.ts index 6d42b5603f212c..98703f18ac3fb2 100644 --- a/packages/deeplinks/search/deep_links.ts +++ b/packages/deeplinks/search/deep_links.ts @@ -20,6 +20,8 @@ import { SERVERLESS_ES_SEARCH_PLAYGROUND_ID, SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID, SEARCH_HOMEPAGE, + SEARCH_INDICES_START, + SEARCH_INDICES, } from './constants'; export type EnterpriseSearchApp = typeof ENTERPRISE_SEARCH_APP_ID; @@ -34,6 +36,8 @@ export type ConnectorsId = typeof SERVERLESS_ES_CONNECTORS_ID; export type SearchPlaygroundId = typeof SERVERLESS_ES_SEARCH_PLAYGROUND_ID; export type SearchInferenceEndpointsId = typeof SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID; export type SearchHomepage = typeof SEARCH_HOMEPAGE; +export type SearchStart = typeof SEARCH_INDICES_START; +export type SearchIndices = typeof SEARCH_INDICES; export type ContentLinkId = 'searchIndices' | 'connectors' | 'webCrawlers'; @@ -59,4 +63,6 @@ export type DeepLinkId = | `${EnterpriseSearchContentApp}:${ContentLinkId}` | `${EnterpriseSearchApplicationsApp}:${ApplicationsLinkId}` | `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}` - | `${EnterpriseSearchRelevanceApp}:${RelevanceLinkId}`; + | `${EnterpriseSearchRelevanceApp}:${RelevanceLinkId}` + | SearchStart + | SearchIndices; diff --git a/x-pack/plugins/search_indices/common/index.ts b/x-pack/plugins/search_indices/common/index.ts index f2b3e62577e4f1..e640397e3936d8 100644 --- a/x-pack/plugins/search_indices/common/index.ts +++ b/x-pack/plugins/search_indices/common/index.ts @@ -5,7 +5,11 @@ * 2.0. */ +import type { SearchIndices, SearchStart } from '@kbn/deeplinks-search/deep_links'; + export const PLUGIN_ID = 'searchIndices'; export const PLUGIN_NAME = 'searchIndices'; +export const START_APP_ID: SearchStart = 'elasticsearchStart'; +export const INDICES_APP_ID: SearchIndices = 'elasticsearchIndices'; export type { IndicesStatusResponse, UserStartPrivilegesResponse } from './types'; diff --git a/x-pack/plugins/search_indices/public/components/start/create_index.tsx b/x-pack/plugins/search_indices/public/components/start/create_index.tsx index bd80922d796893..f1392b3d338136 100644 --- a/x-pack/plugins/search_indices/public/components/start/create_index.tsx +++ b/x-pack/plugins/search_indices/public/components/start/create_index.tsx @@ -56,13 +56,17 @@ export const CreateIndexForm = ({ const [indexNameHasError, setIndexNameHasError] = useState(false); const usageTracker = useUsageTracker(); const { createIndex, isLoading } = useCreateIndex(); - const onCreateIndex = useCallback(() => { - if (!isValidIndexName(formState.indexName)) { - return; - } - usageTracker.click(AnalyticsEvents.startCreateIndexClick); - createIndex({ indexName: formState.indexName }); - }, [usageTracker, createIndex, formState.indexName]); + const onCreateIndex = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!isValidIndexName(formState.indexName)) { + return; + } + usageTracker.click(AnalyticsEvents.startCreateIndexClick); + createIndex({ indexName: formState.indexName }); + }, + [usageTracker, createIndex, formState.indexName] + ); const onIndexNameChange = (e: React.ChangeEvent) => { const newIndexName = e.target.value; setFormState({ ...formState, indexName: e.target.value }); @@ -78,7 +82,12 @@ export const CreateIndexForm = ({ return ( <> - + {CREATE_INDEX_CONTENT} @@ -181,11 +188,7 @@ export const CreateIndexForm = ({ defaultMessage="Already have some data? {link}" values={{ link: ( - + {i18n.translate( 'xpack.searchIndices.startPage.createIndex.fileUpload.link', { diff --git a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx index 00bc11674684a7..8bc8d5fcd7ea26 100644 --- a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx +++ b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx @@ -221,7 +221,6 @@ export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) iconSide="right" iconType="popout" data-test-subj="analyzeLogsBtn" - data-telemetry-id="searchIndicesStartCollectLogsLink" href={docLinks.analyzeLogs} target="_blank" > @@ -249,7 +248,6 @@ export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) iconSide="right" iconType="popout" data-test-subj="startO11yTrialBtn" - data-telemetry-id="searchIndicesStartO11yTrialLink" href={o11yTrialLink} target="_blank" > diff --git a/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx b/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx index d7a24a6e102d82..86fdf75a1d0800 100644 --- a/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx +++ b/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import type { IndicesStatusResponse } from '../../../../common'; @@ -15,13 +15,30 @@ import { navigateToIndexDetails } from './utils'; export const useIndicesRedirect = (indicesStatus?: IndicesStatusResponse) => { const { application, http } = useKibana().services; + const [lastStatus, setLastStatus] = useState(() => undefined); + const [hasDoneRedirect, setHasDoneRedirect] = useState(() => false); return useEffect(() => { - if (!indicesStatus) return; - if (indicesStatus.indexNames.length === 0) return; + if (hasDoneRedirect) { + return; + } + if (!indicesStatus) { + return; + } + if (indicesStatus.indexNames.length === 0) { + setLastStatus(indicesStatus); + return; + } + if (lastStatus === undefined && indicesStatus.indexNames.length > 0) { + application.navigateToApp('management', { deepLinkId: 'index_management' }); + setHasDoneRedirect(true); + return; + } if (indicesStatus.indexNames.length === 1) { navigateToIndexDetails(application, http, indicesStatus.indexNames[0]); + setHasDoneRedirect(true); return; } application.navigateToApp('management', { deepLinkId: 'index_management' }); - }, [application, http, indicesStatus]); + setHasDoneRedirect(true); + }, [application, http, indicesStatus, lastStatus, hasDoneRedirect]); }; diff --git a/x-pack/plugins/search_indices/public/plugin.ts b/x-pack/plugins/search_indices/public/plugin.ts index bec4f7cb7bfe6a..39c3151cf76155 100644 --- a/x-pack/plugins/search_indices/public/plugin.ts +++ b/x-pack/plugins/search_indices/public/plugin.ts @@ -16,6 +16,8 @@ import type { SearchIndicesServicesContextDeps, } from './types'; import { initQueryClient } from './services/query_client'; +import { INDICES_APP_ID, START_APP_ID } from '../common'; +import { INDICES_APP_BASE, START_APP_BASE } from './routes'; export class SearchIndicesPlugin implements Plugin @@ -26,8 +28,8 @@ export class SearchIndicesPlugin const queryClient = initQueryClient(core.notifications.toasts); core.application.register({ - id: 'elasticsearchStart', - appRoute: '/app/elasticsearch/start', + id: START_APP_ID, + appRoute: START_APP_BASE, title: i18n.translate('xpack.searchIndices.elasticsearchStart.startAppTitle', { defaultMessage: 'Elasticsearch Start', }), @@ -43,8 +45,8 @@ export class SearchIndicesPlugin }, }); core.application.register({ - id: 'elasticsearchIndices', - appRoute: '/app/elasticsearch/indices', + id: INDICES_APP_ID, + appRoute: INDICES_APP_BASE, title: i18n.translate('xpack.searchIndices.elasticsearchIndices.startAppTitle', { defaultMessage: 'Elasticsearch Indices', }), @@ -62,12 +64,18 @@ export class SearchIndicesPlugin return { enabled: true, + startAppId: START_APP_ID, + startRoute: START_APP_BASE, }; } public start(core: CoreStart): SearchIndicesPluginStart { docLinks.setDocLinks(core.docLinks.links); - return {}; + return { + enabled: true, + startAppId: START_APP_ID, + startRoute: START_APP_BASE, + }; } public stop() {} diff --git a/x-pack/plugins/search_indices/public/routes.ts b/x-pack/plugins/search_indices/public/routes.ts index 9afa0463855768..c72e84c66a7d0d 100644 --- a/x-pack/plugins/search_indices/public/routes.ts +++ b/x-pack/plugins/search_indices/public/routes.ts @@ -13,3 +13,6 @@ export enum SearchIndexDetailsTabs { MAPPINGS = 'mappings', SETTINGS = 'settings', } + +export const START_APP_BASE = '/app/elasticsearch/start'; +export const INDICES_APP_BASE = '/app/elasticsearch/indices'; diff --git a/x-pack/plugins/search_indices/public/types.ts b/x-pack/plugins/search_indices/public/types.ts index a3e63df2642b3c..ebefaf10cb6eb2 100644 --- a/x-pack/plugins/search_indices/public/types.ts +++ b/x-pack/plugins/search_indices/public/types.ts @@ -15,13 +15,20 @@ import type { MappingProperty, MappingPropertyBase, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IndexManagementPluginStart } from '@kbn/index-management-shared-types'; +import type { IndexManagementPluginStart } from '@kbn/index-management-shared-types'; +import type { AppDeepLinkId } from '@kbn/core-chrome-browser'; export interface SearchIndicesPluginSetup { enabled: boolean; + startAppId: string; + startRoute: string; +} + +export interface SearchIndicesPluginStart { + enabled: boolean; + startAppId: AppDeepLinkId; + startRoute: string; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SearchIndicesPluginStart {} export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; diff --git a/x-pack/plugins/search_indices/tsconfig.json b/x-pack/plugins/search_indices/tsconfig.json index a564b87c7f6e6c..dfd73633b3c3b5 100644 --- a/x-pack/plugins/search_indices/tsconfig.json +++ b/x-pack/plugins/search_indices/tsconfig.json @@ -36,7 +36,9 @@ "@kbn/es-types", "@kbn/search-api-keys-server", "@kbn/search-api-keys-components", - "@kbn/search-shared-ui" + "@kbn/search-shared-ui", + "@kbn/deeplinks-search", + "@kbn/core-chrome-browser" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/serverless_search/public/navigation_tree.ts b/x-pack/plugins/serverless_search/public/navigation_tree.ts index 3fc084b5059d88..9a4ad2a5c51be5 100644 --- a/x-pack/plugins/serverless_search/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_search/public/navigation_tree.ts @@ -5,11 +5,27 @@ * 2.0. */ -import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser'; +import type { + AppDeepLinkId, + NavigationTreeDefinition, + NodeDefinition, +} from '@kbn/core-chrome-browser'; import { i18n } from '@kbn/i18n'; import { CONNECTORS_LABEL } from '../common/i18n_string'; -export const navigationTree = (): NavigationTreeDefinition => ({ +const gettingStartedItem: NodeDefinition = { + id: 'gettingStarted', + title: i18n.translate('xpack.serverlessSearch.nav.gettingStarted', { + defaultMessage: 'Getting Started', + }), + link: 'serverlessElasticsearch', + spaceBefore: 'm', +}; + +export const navigationTree = ( + homeLink: AppDeepLinkId = 'serverlessElasticsearch' as AppDeepLinkId, + showGettingStarted: boolean +): NavigationTreeDefinition => ({ body: [ { type: 'navGroup', @@ -25,7 +41,7 @@ export const navigationTree = (): NavigationTreeDefinition => ({ title: i18n.translate('xpack.serverlessSearch.nav.home', { defaultMessage: 'Home', }), - link: 'serverlessElasticsearch', + link: homeLink, spaceBefore: 'm', }, { @@ -70,6 +86,16 @@ export const navigationTree = (): NavigationTreeDefinition => ({ link: 'management:index_management', breadcrumbStatus: 'hidden' /* management sub-pages set their breadcrumbs themselves */, + getIsActive: ({ pathNameSerialized, prepend }) => { + return ( + pathNameSerialized.startsWith( + prepend('/app/management/data/index_management/') + ) || + pathNameSerialized.startsWith( + prepend('/app/elasticsearch/indices/index_details/') + ) + ); + }, }, { title: CONNECTORS_LABEL, @@ -112,6 +138,7 @@ export const navigationTree = (): NavigationTreeDefinition => ({ }, ], }, + ...(showGettingStarted ? [gettingStartedItem] : []), ], }, ], diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts index 66daece706eab5..211a1cb2384d2a 100644 --- a/x-pack/plugins/serverless_search/public/plugin.ts +++ b/x-pack/plugins/serverless_search/public/plugin.ts @@ -73,6 +73,10 @@ export class ServerlessSearchPlugin const homeTitle = i18n.translate('xpack.serverlessSearch.app.home.title', { defaultMessage: 'Home', }); + const useGlobalEmptyState = setupDeps.searchIndices?.enabled ?? false; + const serverlessElasticsearchAppRoute = useGlobalEmptyState + ? '/app/elasticsearch/getting_started' + : '/app/elasticsearch'; core.application.register({ id: 'serverlessElasticsearch', @@ -81,7 +85,7 @@ export class ServerlessSearchPlugin }), euiIconType: 'logoElastic', category: DEFAULT_APP_CATEGORIES.enterpriseSearch, - appRoute: '/app/elasticsearch', + appRoute: serverlessElasticsearchAppRoute, async mount({ element, history }: AppMountParameters) { const { renderApp } = await import('./application/elasticsearch'); const [coreStart, services] = await core.getStartServices(); @@ -120,6 +124,24 @@ export class ServerlessSearchPlugin }, }); + if (useGlobalEmptyState) { + const redirectApp = setupDeps.searchIndices!.startAppId; + core.application.register({ + id: 'serverlessHomeRedirect', + title: homeTitle, + appRoute: '/app/elasticsearch', + euiIconType: 'logoElastic', + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + visibleIn: [], + async mount({}: AppMountParameters) { + const [coreStart] = await core.getStartServices(); + coreStart.chrome.docTitle.change(homeTitle); + coreStart.application.navigateToApp(redirectApp); + return () => {}; + }, + }); + } + setupDeps.discover.showInlineTopNav(); return {}; @@ -130,10 +152,15 @@ export class ServerlessSearchPlugin services: ServerlessSearchPluginStartDependencies ): ServerlessSearchPluginStart { const { serverless, management, indexManagement, security } = services; - - serverless.setProjectHome('/app/elasticsearch'); - - const navigationTree$ = of(navigationTree()); + const useGlobalEmptyState = services.searchIndices?.enabled ?? false; + const homeRoute = useGlobalEmptyState + ? services.searchIndices!.startRoute + : '/app/elasticsearch'; + serverless.setProjectHome(homeRoute); + + const navigationTree$ = of( + navigationTree(services.searchIndices?.startAppId, useGlobalEmptyState) + ); serverless.initNavigation('search', navigationTree$, { dataTestSubj: 'svlSearchSideNav' }); const extendCardNavDefinitions = serverless.getNavigationCards( diff --git a/x-pack/plugins/serverless_search/public/types.ts b/x-pack/plugins/serverless_search/public/types.ts index 7067851bc01967..65952c963a2a45 100644 --- a/x-pack/plugins/serverless_search/public/types.ts +++ b/x-pack/plugins/serverless_search/public/types.ts @@ -15,6 +15,10 @@ import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverle import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { IndexManagementPluginStart } from '@kbn/index-management-plugin/public'; import type { DiscoverSetup } from '@kbn/discover-plugin/public'; +import type { + SearchIndicesPluginSetup, + SearchIndicesPluginStart, +} from '@kbn/search-indices/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ServerlessSearchPluginSetup {} @@ -27,6 +31,7 @@ export interface ServerlessSearchPluginSetupDependencies { management: ManagementSetup; serverless: ServerlessPluginSetup; discover: DiscoverSetup; + searchIndices?: SearchIndicesPluginSetup; } export interface ServerlessSearchPluginStartDependencies { @@ -39,4 +44,5 @@ export interface ServerlessSearchPluginStartDependencies { serverless: ServerlessPluginStart; share: SharePluginStart; indexManagement?: IndexManagementPluginStart; + searchIndices?: SearchIndicesPluginStart; } diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json index c856b155acc7cc..cc3b7b073dcee0 100644 --- a/x-pack/plugins/serverless_search/tsconfig.json +++ b/x-pack/plugins/serverless_search/tsconfig.json @@ -51,5 +51,6 @@ "@kbn/security-api-key-management", "@kbn/search-inference-endpoints", "@kbn/security-plugin-types-common", + "@kbn/search-indices", ] } diff --git a/x-pack/test_serverless/functional/config.base.ts b/x-pack/test_serverless/functional/config.base.ts index 826b8bce038855..963be692d58ea4 100644 --- a/x-pack/test_serverless/functional/config.base.ts +++ b/x-pack/test_serverless/functional/config.base.ts @@ -116,6 +116,7 @@ export function createTestConfig(options: CreateTestConfigOptions) { integrations: { pathname: '/app/integrations', }, + ...(options.apps ?? {}), }, // choose where screenshots should be saved screenshots: { diff --git a/x-pack/test_serverless/functional/services/svl_search_navigation.ts b/x-pack/test_serverless/functional/services/svl_search_navigation.ts index 9de765cabd6bb7..cf91aac1127738 100644 --- a/x-pack/test_serverless/functional/services/svl_search_navigation.ts +++ b/x-pack/test_serverless/functional/services/svl_search_navigation.ts @@ -22,12 +22,20 @@ export function SvlSearchNavigationServiceProvider({ await testSubjects.existOrFail('svlSearchOverviewPage', { timeout: 2000 }); }); }, - async navigateToElasticsearchStartPage() { + async navigateToGettingStartedPage() { await retry.tryForTime(60 * 1000, async () => { - await PageObjects.common.navigateToApp('elasticsearch/start', { + await PageObjects.common.navigateToApp('serverlessElasticsearch'); + await testSubjects.existOrFail('svlSearchOverviewPage', { timeout: 2000 }); + }); + }, + async navigateToElasticsearchStartPage(expectRedirect: boolean = false) { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp('elasticsearchStart', { shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('elasticsearchStartPage', { timeout: 2000 }); + if (!expectRedirect) { + await testSubjects.existOrFail('elasticsearchStartPage', { timeout: 2000 }); + } }); }, async navigateToIndexDetailPage(indexName: string) { diff --git a/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts index 824d145282257b..ebd539fd34f42f 100644 --- a/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts +++ b/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts @@ -34,4 +34,15 @@ export default createTestConfig({ // include settings from project controller // https://github.com/elastic/project-controller/blob/main/internal/project/esproject/config/elasticsearch.yml esServerArgs: ['xpack.security.authc.native_roles.enabled=true'], + apps: { + serverlessElasticsearch: { + pathname: '/app/elasticsearch/getting_started', + }, + elasticsearchStart: { + pathname: '/app/elasticsearch/start', + }, + elasticsearchIndices: { + pathname: '/app/elasticsearch/indices', + }, + }, }); diff --git a/x-pack/test_serverless/functional/test_suites/search/config.ts b/x-pack/test_serverless/functional/test_suites/search/config.ts index 4739cde53bf860..678adecd4bff56 100644 --- a/x-pack/test_serverless/functional/test_suites/search/config.ts +++ b/x-pack/test_serverless/functional/test_suites/search/config.ts @@ -24,4 +24,15 @@ export default createTestConfig({ `--xpack.cloud.base_url=https://fake-cloud.elastic.co`, `--xpack.cloud.projects_url=/projects/`, ], + apps: { + serverlessElasticsearch: { + pathname: '/app/elasticsearch', + }, + serverlessConnectors: { + pathname: '/app/connectors', + }, + searchPlayground: { + pathname: '/app/search_playground', + }, + }, }); diff --git a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts index 9a4b72cf5126f5..d9d4389d4d63c0 100644 --- a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts +++ b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts @@ -26,8 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esDeleteAllIndices(['search-*', 'test-*']); }; - // Failing: See https://github.com/elastic/kibana/issues/194673 - describe.skip('Elasticsearch Start [Onboarding Empty State]', function () { + describe('Elasticsearch Start [Onboarding Empty State]', function () { describe('developer', function () { before(async () => { await pageObjects.svlCommonPage.loginWithRole('developer'); @@ -77,6 +76,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await es.indices.create({ index: 'test-my-index-002' }); await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexListPage(); }); + it('should redirect to indices list if single index exist on page load', async () => { + await svlSearchNavigation.navigateToGettingStartedPage(); + await es.indices.create({ index: 'test-my-index-001' }); + await svlSearchNavigation.navigateToElasticsearchStartPage(true); + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexListPage(); + }); it('should support switching between UI and Code Views', async () => { await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); @@ -87,7 +92,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView(); }); - it('should show the api key in code view', async () => { + // Failing: See https://github.com/elastic/kibana/issues/194673 + it.skip('should show the api key in code view', async () => { await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton(); await pageObjects.svlApiKeys.expectAPIKeyAvailable(); @@ -125,7 +131,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlApiKeys.expectAPIKeyAvailable(); }); - it('Same API Key should be present on start page and index detail view', async () => { + // Failing: See https://github.com/elastic/kibana/issues/194673 + it.skip('Same API Key should be present on start page and index detail view', async () => { await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton(); await pageObjects.svlApiKeys.expectAPIKeyAvailable(); const apiKeyUI = await pageObjects.svlApiKeys.getAPIKeyFromUI(); @@ -151,41 +158,40 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchElasticsearchStartPage.expectAnalyzeLogsLink(); await pageObjects.svlSearchElasticsearchStartPage.expectO11yTrialLink(); }); + }); + describe('viewer', function () { + before(async () => { + await pageObjects.svlCommonPage.loginAsViewer(); + await deleteAllTestIndices(); + }); + beforeEach(async () => { + await svlSearchNavigation.navigateToElasticsearchStartPage(); + }); + after(async () => { + await deleteAllTestIndices(); + }); + + it('should default to code view when lacking create index permissions', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView(); + await pageObjects.svlSearchElasticsearchStartPage.clickUIViewButton(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeDisabled(); + }); + + it('should not create an API key if the user only has viewer permissions', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton(); + await pageObjects.svlSearchElasticsearchStartPage.expectAPIKeyFormNotAvailable(); + const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromSessionStorage(); + expect(apiKey).to.be(null); + }); - describe('viewer', function () { - before(async () => { - await pageObjects.svlCommonPage.loginAsViewer(); - await deleteAllTestIndices(); - }); - beforeEach(async () => { - await svlSearchNavigation.navigateToElasticsearchStartPage(); - }); - after(async () => { - await deleteAllTestIndices(); - }); - - it('should default to code view when lacking create index permissions', async () => { - await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); - await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView(); - await pageObjects.svlSearchElasticsearchStartPage.clickUIViewButton(); - await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView(); - await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeDisabled(); - }); - - it('should not create an API key if the user only has viewer permissions', async () => { - await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); - await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton(); - await pageObjects.svlSearchElasticsearchStartPage.expectAPIKeyFormNotAvailable(); - const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromSessionStorage(); - expect(apiKey).to.be(null); - }); - - it('should redirect to index details when index is created via API', async () => { - await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); - await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView(); - await es.indices.create({ index: 'test-my-api-index' }); - await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage(); - }); + it('should redirect to index details when index is created via API', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView(); + await es.indices.create({ index: 'test-my-api-index' }); + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage(); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/search/landing_page.ts b/x-pack/test_serverless/functional/test_suites/search/getting_started.ts similarity index 95% rename from x-pack/test_serverless/functional/test_suites/search/landing_page.ts rename to x-pack/test_serverless/functional/test_suites/search/getting_started.ts index 643d163bf76722..f521a03ccde850 100644 --- a/x-pack/test_serverless/functional/test_suites/search/landing_page.ts +++ b/x-pack/test_serverless/functional/test_suites/search/getting_started.ts @@ -13,13 +13,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['svlSearchLandingPage', 'svlCommonPage', 'embeddedConsole']); const svlSearchNavigation = getService('svlSearchNavigation'); - describe('landing page', function () { + describe('getting started', function () { before(async () => { await pageObjects.svlCommonPage.loginAsViewer(); }); it('has serverless side nav', async () => { - await svlSearchNavigation.navigateToLandingPage(); + await svlSearchNavigation.navigateToGettingStartedPage(); await pageObjects.svlSearchLandingPage.assertSvlSearchSideNavExists(); }); @@ -44,7 +44,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('API Key creation', () => { beforeEach(async () => { // We need to reload the page between api key creations - await svlSearchNavigation.navigateToLandingPage(); + await svlSearchNavigation.navigateToGettingStartedPage(); }); it('can create an API key that expires', async () => { @@ -86,7 +86,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Pipelines', () => { beforeEach(async () => { - await svlSearchNavigation.navigateToLandingPage(); + await svlSearchNavigation.navigateToGettingStartedPage(); }); it('can redirect to the pipeline creation index page', async () => { await pageObjects.svlSearchLandingPage.pipeline.createPipeline(); diff --git a/x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts index c5c9b82dd21cd5..db9df2e8d913cb 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts @@ -12,6 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { // add tests that require feature flags, defined in config.feature_flags.ts loadTestFile(require.resolve('./elasticsearch_start.ts')); loadTestFile(require.resolve('./search_index_detail.ts')); + loadTestFile(require.resolve('./getting_started')); loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts')); loadTestFile(require.resolve('../common/platform_security/roles.ts')); loadTestFile(require.resolve('../common/spaces/multiple_spaces_enabled.ts')); diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index bc2056268f8bd7..250f99d13a3a12 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -11,7 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless search UI', function () { this.tags(['esGate']); - loadTestFile(require.resolve('./landing_page')); + loadTestFile(require.resolve('./getting_started')); loadTestFile(require.resolve('./connectors/connectors_overview')); loadTestFile(require.resolve('./default_dataview')); loadTestFile(require.resolve('./navigation')); diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index db855e9522f9a1..0f1ee8334e2038 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -84,7 +84,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); }); - it('should show api key', async () => { + // Failing: See https://github.com/elastic/kibana/issues/194673 + it.skip('should show api key', async () => { await pageObjects.svlApiKeys.expectAPIKeyAvailable(); const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromUI(); await pageObjects.svlSearchIndexDetailPage.expectAPIKeyToBeVisibleInCodeBlock(apiKey); diff --git a/x-pack/test_serverless/shared/types/index.ts b/x-pack/test_serverless/shared/types/index.ts index e07c649d86cb9c..2545bc0d30539b 100644 --- a/x-pack/test_serverless/shared/types/index.ts +++ b/x-pack/test_serverless/shared/types/index.ts @@ -16,4 +16,5 @@ export interface CreateTestConfigOptions { junit: { reportName: string }; suiteTags?: { include?: string[]; exclude?: string[] }; services?: InheritedServices; + apps?: Record; } From 14d5677d073838845074f76f0d9c0edce50533f0 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 8 Oct 2024 16:36:19 +0200 Subject: [PATCH 09/50] [Fleet] Adjust privileges for GET output and GET download_source endpoints (#194951) Fixes https://github.com/elastic/kibana/issues/191266 ## Summary Updating the authz for following endpoints: - `GET /agent_download_sources` - `GET /agent_download_sources/{id}` - `GET /outputs` - `GET /outputs/{id}` They need to have `authz.fleet.readSettings || authz.fleet.readAgentPolicies` as they should be visible in the agent policy settings page as well. ### Testing - Enable feature flag `subfeaturePrivileges` - Create a role with following privileges: ![Screenshot 2024-10-04 at 15 49 54](https://github.com/user-attachments/assets/4bbc95e4-01d0-43e0-a539-b03b8f4c219e) - Create a user that has the previous role - Log in and go to any agent policy > settings - The download source and output fields should be filled and editable. They were previously empty, as the GET endpoints were failing with 403 Forbidden ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine --- .../server/routes/download_source/index.ts | 8 +-- .../fleet/server/routes/output/index.ts | 12 ++--- .../apis/fleet_settings_privileges.ts | 51 +++++++++++++++++-- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/download_source/index.ts b/x-pack/plugins/fleet/server/routes/download_source/index.ts index cf2a5b19c4b473..83059593730db2 100644 --- a/x-pack/plugins/fleet/server/routes/download_source/index.ts +++ b/x-pack/plugins/fleet/server/routes/download_source/index.ts @@ -36,8 +36,8 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .get({ path: DOWNLOAD_SOURCE_API_ROUTES.LIST_PATTERN, - fleetAuthz: { - fleet: { readSettings: true }, + fleetAuthz: (authz) => { + return authz.fleet.readSettings || authz.fleet.readAgentPolicies; }, description: `List agent binary download sources`, options: { @@ -65,8 +65,8 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .get({ path: DOWNLOAD_SOURCE_API_ROUTES.INFO_PATTERN, - fleetAuthz: { - fleet: { readSettings: true }, + fleetAuthz: (authz) => { + return authz.fleet.readSettings || authz.fleet.readAgentPolicies; }, description: `Get agent binary download source by ID`, options: { diff --git a/x-pack/plugins/fleet/server/routes/output/index.ts b/x-pack/plugins/fleet/server/routes/output/index.ts index b222f9f737d1df..a90735f053208e 100644 --- a/x-pack/plugins/fleet/server/routes/output/index.ts +++ b/x-pack/plugins/fleet/server/routes/output/index.ts @@ -40,8 +40,8 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .get({ path: OUTPUT_API_ROUTES.LIST_PATTERN, - fleetAuthz: { - fleet: { readSettings: true }, + fleetAuthz: (authz) => { + return authz.fleet.readSettings || authz.fleet.readAgentPolicies; }, description: 'List outputs', options: { @@ -68,8 +68,8 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .get({ path: OUTPUT_API_ROUTES.INFO_PATTERN, - fleetAuthz: { - fleet: { readSettings: true }, + fleetAuthz: (authz) => { + return authz.fleet.readSettings || authz.fleet.readAgentPolicies; }, description: 'Get output by ID', options: { @@ -96,8 +96,8 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .put({ path: OUTPUT_API_ROUTES.UPDATE_PATTERN, - fleetAuthz: { - fleet: { allSettings: true }, + fleetAuthz: (authz) => { + return authz.fleet.allSettings || authz.fleet.allAgentPolicies; }, description: 'Update output by ID', options: { diff --git a/x-pack/test/fleet_api_integration/apis/fleet_settings_privileges.ts b/x-pack/test/fleet_api_integration/apis/fleet_settings_privileges.ts index 12399d5ba9bf2a..7d6a58c0661212 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_settings_privileges.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_settings_privileges.ts @@ -51,6 +51,49 @@ const READ_SCENARIOS = [ statusCode: 403, }, ]; +// Scenarios updated for download_source and outputs routes that are slightly different +const READ_SCENARIOS_2 = [ + { + user: testUsers.fleet_all_only, + statusCode: 200, + }, + { + user: testUsers.fleet_read_only, + statusCode: 200, + }, + { + user: testUsers.fleet_settings_all_only, + statusCode: 200, + }, + { + user: testUsers.fleet_settings_read_only, + statusCode: 200, + }, + { + user: testUsers.fleet_agent_policies_read_only, + statusCode: 200, + }, + { + user: testUsers.fleet_agent_policies_all_only, + statusCode: 200, + }, + { + user: testUsers.fleet_agents_read_only, + statusCode: 403, + }, + { + user: testUsers.fleet_no_access, + statusCode: 403, + }, + { + user: testUsers.fleet_minimal_all_only, + statusCode: 403, + }, + { + user: testUsers.fleet_minimal_read_only, + statusCode: 403, + }, +]; const ALL_SCENARIOS = [ { @@ -106,12 +149,12 @@ export default function (providerContext: FtrProviderContext) { { method: 'GET', path: '/api/fleet/outputs', - scenarios: READ_SCENARIOS, + scenarios: READ_SCENARIOS_2, }, { method: 'GET', path: '/api/fleet/outputs/test-privileges-output-1', - scenarios: READ_SCENARIOS, + scenarios: READ_SCENARIOS_2, }, { method: 'POST', @@ -226,12 +269,12 @@ export default function (providerContext: FtrProviderContext) { { method: 'GET', path: '/api/fleet/agent_download_sources', - scenarios: READ_SCENARIOS, + scenarios: READ_SCENARIOS_2, }, { method: 'GET', path: '/api/fleet/agent_download_sources/test-privileges-download-source-1', - scenarios: READ_SCENARIOS, + scenarios: READ_SCENARIOS_2, }, { method: 'POST', From da88bb2c01c7c2748ac60e233afd687b9ed32cf2 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Tue, 8 Oct 2024 10:37:04 -0400 Subject: [PATCH 10/50] [Fleet] Update GitHub action to route Fleet issues to new project (#195417) ## Summary The UI & Integrations team is moving to a new GitHub project. This PR updates our automation to automatically place issues in the new project instead of the old one. --- .github/workflows/add-fleet-issues-to-ingest-project.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/add-fleet-issues-to-ingest-project.yml b/.github/workflows/add-fleet-issues-to-ingest-project.yml index ca04c0e6fa6d74..86c42d95ed403c 100644 --- a/.github/workflows/add-fleet-issues-to-ingest-project.yml +++ b/.github/workflows/add-fleet-issues-to-ingest-project.yml @@ -1,4 +1,4 @@ -name: Add Fleet issue to Platform Ingest project +name: Add Fleet issue to Platform Ingest - UI & Integrations project on: issues: @@ -6,9 +6,9 @@ on: - labeled env: - INGEST_PROJECT_ID: 'PVT_kwDOAGc3Zs4AEzn4' + INGEST_PROJECT_ID: 'PVT_kwDOAGc3Zs4AogrP' FLEET_LABEL: 'Team:Fleet' - AREA_FIELD_ID: 'PVTSSF_lADOAGc3Zs4AEzn4zgEgZSo' + AREA_FIELD_ID: 'PVTSSF_lADOAGc3Zs4AogrPzggEiBs' FLEET_UI_OPTION_ID: '411a7b86' jobs: From 560d561e21fe51020954bd9e3246f238ffa026ba Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 8 Oct 2024 10:46:30 -0400 Subject: [PATCH 11/50] [Synthetics] Fix ping heatmap payload (#195107) ## Summary We addressed https://github.com/elastic/kibana/issues/180076 recently with these two PRs: - https://github.com/elastic/kibana/pull/184177 - https://github.com/elastic/kibana/pull/192508 We were seeing a strange error that was difficult to repro, so we put in a best-effort patch that was still ineffective. The reason this issue happens is because in the code it's possible to divide by 0, which yields a value of `Infinity`, which at some point causes our interval value supplied to the server route to be an empty string. This patch will make it so that we never pass a value of 0 to be used in the calculation of bucket sizes in this hook. --- .../monitor_details/monitor_status/use_monitor_status_data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts index efb001f1776b7f..8eaa80fb44a53d 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts @@ -93,7 +93,7 @@ export const useMonitorStatusData = ({ from, to, initialSizeRef }: Props) => { useDebounce( async () => { - setDebouncedCount(binsAvailableByWidth); + setDebouncedCount(binsAvailableByWidth === 0 ? null : binsAvailableByWidth); }, 500, [binsAvailableByWidth] From 149e801109d7c9edf2d6eef41ebb0e281314a19f Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 8 Oct 2024 16:52:21 +0200 Subject: [PATCH 12/50] [ES|QL] Recommended queries (#194418) ## Summary Closes https://github.com/elastic/kibana/issues/187325 image image This is the first iteration of this feature. We want to help the users familiarize themselves with popular operations. This PR: - adds the recommended queries list in the help menu of unified search - adds the list after the users select a datasource with the from command - adds the list in the editor's empty state ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../kbn-esql-validation-autocomplete/index.ts | 2 + .../autocomplete.command.from.test.ts | 12 +- .../__tests__/autocomplete.suggest.test.ts | 5 +- .../src/autocomplete/autocomplete.test.ts | 57 ++++-- .../src/autocomplete/autocomplete.ts | 49 ++++- .../src/autocomplete/helper.ts | 4 +- .../recommended_queries/suggestions.ts | 40 ++++ .../recommended_queries/templates.ts | 129 +++++++++++++ .../src/autocomplete/types.ts | 6 + .../src/definitions/commands.ts | 1 + .../src/definitions/types.ts | 1 + .../esql_menu_popover.test.tsx | 14 +- .../query_string_input/esql_menu_popover.tsx | 179 ++++++++++++------ .../query_string_input/query_bar_top_row.tsx | 7 + .../public/search_bar/search_bar.test.tsx | 39 ++-- src/plugins/unified_search/tsconfig.json | 1 + 16 files changed, 424 insertions(+), 122 deletions(-) create mode 100644 packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts create mode 100644 packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts diff --git a/packages/kbn-esql-validation-autocomplete/index.ts b/packages/kbn-esql-validation-autocomplete/index.ts index bd41d6ec43a5ad..0bfc4274fe84de 100644 --- a/packages/kbn-esql-validation-autocomplete/index.ts +++ b/packages/kbn-esql-validation-autocomplete/index.ts @@ -76,3 +76,5 @@ export { } from './src/shared/resources_helpers'; export { wrapAsEditorMessage } from './src/code_actions/utils'; + +export { getRecommendedQueries } from './src/autocomplete/recommended_queries/templates'; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.from.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.from.test.ts index fa2a969384a09c..fa2e81ded897e6 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.from.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.from.test.ts @@ -9,6 +9,7 @@ import { METADATA_FIELDS } from '../../shared/constants'; import { setup, indexes, integrations } from './helpers'; +import { getRecommendedQueries } from '../recommended_queries/templates'; const visibleIndices = indexes .filter(({ hidden }) => !hidden) @@ -72,8 +73,17 @@ describe('autocomplete.suggest', () => { const metadataFieldsAndIndex = metadataFields.filter((field) => field !== '_index'); test('on SPACE without comma ",", suggests adding metadata', async () => { + const recommendedQueries = getRecommendedQueries({ + fromCommand: '', + timeField: 'dateField', + }); const { assertSuggestions } = await setup(); - const expected = ['METADATA $0', ',', '| '].sort(); + const expected = [ + 'METADATA $0', + ',', + '| ', + ...recommendedQueries.map((query) => query.queryString), + ].sort(); await assertSuggestions('from a, b /', expected); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts index c4803e769c1fc0..51302d0d4cde55 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts @@ -36,12 +36,9 @@ const setup = async (caret = '?') => { }; describe('autocomplete.suggest', () => { - test('does not load fields when suggesting within a single FROM, SHOW, ROW command', async () => { + test('does not load fields when suggesting within a single SHOW, ROW command', async () => { const { suggest, callbacks } = await setup(); - await suggest('FROM kib, ? |'); - await suggest('FROM ?'); - await suggest('FROM ? |'); await suggest('sHoW ?'); await suggest('row ? |'); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 095cb2ffc9d145..84779f1dd36b5e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -35,8 +35,16 @@ import { import { METADATA_FIELDS } from '../shared/constants'; import { ESQL_COMMON_NUMERIC_TYPES, ESQL_STRING_TYPES } from '../shared/esql_types'; import { log10ParameterTypes, powParameterTypes } from './__tests__/constants'; +import { getRecommendedQueries } from './recommended_queries/templates'; const commandDefinitions = unmodifiedCommandDefinitions.filter(({ hidden }) => !hidden); + +const getRecommendedQueriesSuggestions = (fromCommand: string, timeField?: string) => + getRecommendedQueries({ + fromCommand, + timeField, + }); + describe('autocomplete', () => { type TestArgs = [ string, @@ -82,10 +90,11 @@ describe('autocomplete', () => { const sourceCommands = ['row', 'from', 'show']; describe('New command', () => { - testSuggestions( - '/', - sourceCommands.map((name) => name.toUpperCase() + ' $0') - ); + const recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField'); + testSuggestions('/', [ + ...sourceCommands.map((name) => name.toUpperCase() + ' $0'), + ...recommendedQuerySuggestions.map((q) => q.queryString), + ]); testSuggestions( 'from a | /', commandDefinitions @@ -523,10 +532,11 @@ describe('autocomplete', () => { */ describe('Invoke trigger kind (all commands)', () => { // source command - testSuggestions( - 'f/', - sourceCommands.map((cmd) => `${cmd.toUpperCase()} $0`) - ); + let recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField'); + testSuggestions('f/', [ + ...sourceCommands.map((cmd) => `${cmd.toUpperCase()} $0`), + ...recommendedQuerySuggestions.map((q) => q.queryString), + ]); // pipe command testSuggestions( @@ -575,7 +585,13 @@ describe('autocomplete', () => { ]); // FROM source METADATA - testSuggestions('FROM index1 M/', [',', 'METADATA $0', '| ']); + recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField'); + testSuggestions('FROM index1 M/', [ + ',', + 'METADATA $0', + '| ', + ...recommendedQuerySuggestions.map((q) => q.queryString), + ]); // FROM source METADATA field testSuggestions('FROM index1 METADATA _/', METADATA_FIELDS); @@ -710,12 +726,12 @@ describe('autocomplete', () => { ...s, asSnippet: true, }); - + let recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField'); // Source command - testSuggestions( - 'F/', - ['FROM $0', 'ROW $0', 'SHOW $0'].map(attachTriggerCommand).map(attachAsSnippet) - ); + testSuggestions('F/', [ + ...['FROM $0', 'ROW $0', 'SHOW $0'].map(attachTriggerCommand).map(attachAsSnippet), + ...recommendedQuerySuggestions.map((q) => q.queryString), + ]); // Pipe command testSuggestions( @@ -787,11 +803,14 @@ describe('autocomplete', () => { ); }); + recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField'); + // PIPE (|) testSuggestions('FROM a /', [ attachTriggerCommand('| '), ',', attachAsSnippet(attachTriggerCommand('METADATA $0')), + ...recommendedQuerySuggestions.map((q) => q.queryString), ]); // Assignment @@ -833,6 +852,7 @@ describe('autocomplete', () => { ], ] ); + recommendedQuerySuggestions = getRecommendedQueriesSuggestions('index1', 'dateField'); testSuggestions( 'FROM index1/', @@ -840,6 +860,7 @@ describe('autocomplete', () => { { text: 'index1 | ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND }, { text: 'index1, ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND }, { text: 'index1 METADATA ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND }, + ...recommendedQuerySuggestions.map((q) => q.queryString), ], undefined, [ @@ -851,12 +872,14 @@ describe('autocomplete', () => { ] ); + recommendedQuerySuggestions = getRecommendedQueriesSuggestions('index2', 'dateField'); testSuggestions( 'FROM index1, index2/', [ { text: 'index2 | ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND }, { text: 'index2, ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND }, { text: 'index2 METADATA ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND }, + ...recommendedQuerySuggestions.map((q) => q.queryString), ], undefined, [ @@ -872,6 +895,7 @@ describe('autocomplete', () => { // meaning that Monaco by default will only set the replacement // range to cover "bar" and not "foo$bar". We have to make sure // we're setting it ourselves. + recommendedQuerySuggestions = getRecommendedQueriesSuggestions('foo$bar', 'dateField'); testSuggestions( 'FROM foo$bar/', [ @@ -894,18 +918,21 @@ describe('autocomplete', () => { command: TRIGGER_SUGGESTION_COMMAND, rangeToReplace: { start: 6, end: 13 }, }, + ...recommendedQuerySuggestions.map((q) => q.queryString), ], undefined, [, [{ name: 'foo$bar', hidden: false }]] ); // This is an identifier that matches multiple sources + recommendedQuerySuggestions = getRecommendedQueriesSuggestions('i*', 'dateField'); testSuggestions( 'FROM i*/', [ { text: 'i* | ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND }, { text: 'i*, ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND }, { text: 'i* METADATA ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND }, + ...recommendedQuerySuggestions.map((q) => q.queryString), ], undefined, [ @@ -918,11 +945,13 @@ describe('autocomplete', () => { ); }); + recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField'); // FROM source METADATA testSuggestions('FROM index1 M/', [ ',', attachAsSnippet(attachTriggerCommand('METADATA $0')), '| ', + ...recommendedQuerySuggestions.map((q) => q.queryString), ]); describe('ENRICH', () => { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 41060675a73a84..2433f5d4965217 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -19,7 +19,7 @@ import type { } from '@kbn/esql-ast'; import { i18n } from '@kbn/i18n'; import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types'; -import type { EditorContext, ItemKind, SuggestionRawDefinition } from './types'; +import type { EditorContext, ItemKind, SuggestionRawDefinition, GetFieldsByTypeFn } from './types'; import { getColumnForASTNode, getCommandDefinition, @@ -113,12 +113,8 @@ import { import { metadataOption } from '../definitions/options'; import { comparisonFunctions } from '../definitions/builtin'; import { countBracketsUnclosed } from '../shared/helpers'; +import { getRecommendedQueriesSuggestions } from './recommended_queries/suggestions'; -type GetFieldsByTypeFn = ( - type: string | string[], - ignored?: string[], - options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean } -) => Promise; type GetFieldsMapFn = () => Promise>; type GetPoliciesFn = () => Promise; type GetPolicyMetadataFn = (name: string) => Promise; @@ -176,7 +172,7 @@ export async function suggest( ); const { getFieldsByType, getFieldsMap } = getFieldsByTypeRetriever( - queryForFields, + queryForFields.replace(EDITOR_MARKER, ''), resourceRetriever ); const getSources = getSourcesHelper(resourceRetriever); @@ -187,7 +183,26 @@ export async function suggest( // filter source commands if already defined const suggestions = commandAutocompleteDefinitions; if (!ast.length) { - return suggestions.filter(isSourceCommand); + // Display the recommended queries if there are no commands (empty state) + const recommendedQueriesSuggestions: SuggestionRawDefinition[] = []; + if (getSources) { + let fromCommand = ''; + const sources = await getSources(); + const visibleSources = sources.filter((source) => !source.hidden); + if (visibleSources.find((source) => source.name.startsWith('logs'))) { + fromCommand = 'FROM logs*'; + } else fromCommand = `FROM ${visibleSources[0].name}`; + + const { getFieldsByType: getFieldsByTypeEmptyState } = getFieldsByTypeRetriever( + fromCommand, + resourceRetriever + ); + recommendedQueriesSuggestions.push( + ...(await getRecommendedQueriesSuggestions(getFieldsByTypeEmptyState, fromCommand)) + ); + } + const sourceCommandsSuggestions = suggestions.filter(isSourceCommand); + return [...sourceCommandsSuggestions, ...recommendedQueriesSuggestions]; } return suggestions.filter((def) => !isSourceCommand(def)); @@ -519,6 +534,7 @@ async function getExpressionSuggestionsByType( const optArg = optionsAlreadyDeclared.find(({ name: optionName }) => optionName === name); return (!optArg && !optionsAlreadyDeclared.length) || (optArg && index > optArg.index); }); + const hasRecommendedQueries = Boolean(commandDef?.hasRecommendedQueries); // get the next definition for the given command let argDef = commandDef.signature.params[argIndex]; // tune it for the variadic case @@ -910,6 +926,11 @@ async function getExpressionSuggestionsByType( if (lastIndex && lastIndex.text && lastIndex.text !== EDITOR_MARKER) { const sources = await getSources(); + + const recommendedQueriesSuggestions = hasRecommendedQueries + ? await getRecommendedQueriesSuggestions(getFieldsByType) + : []; + const suggestionsToAdd = await handleFragment( innerText, (fragment) => @@ -952,8 +973,13 @@ async function getExpressionSuggestionsByType( asSnippet: false, // turn this off because $ could be contained within the source name rangeToReplace, }, + ...recommendedQueriesSuggestions.map((suggestion) => ({ + ...suggestion, + rangeToReplace, + filterText: fragment, + text: fragment + suggestion.text, + })), ]; - return _suggestions; } } @@ -1005,6 +1031,11 @@ async function getExpressionSuggestionsByType( })); suggestions.push(...finalSuggestions); } + + // handle recommended queries for from + if (hasRecommendedQueries) { + suggestions.push(...(await getRecommendedQueriesSuggestions(getFieldsByType))); + } } // Due to some logic overlapping functions can be repeated // so dedupe here based on text string (it can differ from name) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts index f42d7de5a38ab2..41f6a92dc313d7 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts @@ -86,9 +86,7 @@ export function strictlyGetParamAtPosition( export function getQueryForFields(queryString: string, commands: ESQLCommand[]) { // If there is only one source command and it does not require fields, do not // fetch fields, hence return an empty string. - return commands.length === 1 && ['from', 'row', 'show'].includes(commands[0].name) - ? '' - : queryString; + return commands.length === 1 && ['row', 'show'].includes(commands[0].name) ? '' : queryString; } export function getSourcesFromCommands(commands: ESQLCommand[], sourceType: 'index' | 'policy') { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts new file mode 100644 index 00000000000000..fbcfbabb2b63c9 --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { SuggestionRawDefinition, GetFieldsByTypeFn } from '../types'; +import { getRecommendedQueries } from './templates'; + +export const getRecommendedQueriesSuggestions = async ( + getFieldsByType: GetFieldsByTypeFn, + fromCommand: string = '' +): Promise => { + const fieldSuggestions = await getFieldsByType('date', [], { + openSuggestions: true, + }); + let timeField = ''; + if (fieldSuggestions.length) { + timeField = + fieldSuggestions?.find((field) => field.label === '@timestamp')?.label || + fieldSuggestions[0].label; + } + + const recommendedQueries = getRecommendedQueries({ fromCommand, timeField }); + + const suggestions: SuggestionRawDefinition[] = recommendedQueries.map((query) => { + return { + label: query.label, + text: query.queryString, + kind: 'Issue', + detail: query.description, + sortText: 'D', + }; + }); + + return suggestions; +}; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts new file mode 100644 index 00000000000000..f910d3ba05a3be --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/templates.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +// Order starts with the simple ones and goes to more complex ones + +export const getRecommendedQueries = ({ + fromCommand, + timeField, +}: { + fromCommand: string; + timeField?: string; +}) => { + const queries = [ + { + label: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.aggregateExample.label', + { + defaultMessage: 'Aggregate with STATS', + } + ), + description: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.aggregateExample.description', + { + defaultMessage: 'Count aggregation', + } + ), + queryString: `${fromCommand}\n | STATS count = COUNT(*) // you can group by a field using the BY operator`, + }, + ...(timeField + ? [ + { + label: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.sortByTime.label', + { + defaultMessage: 'Sort by time', + } + ), + description: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.sortByTime.description', + { + defaultMessage: 'Sort by time', + } + ), + queryString: `${fromCommand}\n | SORT ${timeField} // Data is not sorted by default`, + }, + { + label: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.dateIntervals.label', + { + defaultMessage: 'Create 5 minute time buckets with EVAL', + } + ), + description: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.dateIntervals.description', + { + defaultMessage: 'Count aggregation over time', + } + ), + queryString: `${fromCommand}\n | EVAL buckets = DATE_TRUNC(5 minute, ${timeField}) | STATS count = COUNT(*) BY buckets // try out different intervals`, + }, + ] + : []), + { + label: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.caseExample.label', + { + defaultMessage: 'Create a conditional with CASE', + } + ), + description: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.caseExample.description', + { + defaultMessage: 'Conditional', + } + ), + queryString: `${fromCommand}\n | STATS count = COUNT(*)\n | EVAL newField = CASE(count < 100, "groupA", count > 100 and count < 500, "groupB", "Other")\n | KEEP newField`, + }, + ...(timeField + ? [ + { + label: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.dateHistogram.label', + { + defaultMessage: 'Create a date histogram', + } + ), + description: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.dateHistogram.description', + { + defaultMessage: 'Count aggregation over time', + } + ), + queryString: `${fromCommand}\n | WHERE ${timeField} <=?_tend and ${timeField} >?_tstart\n | STATS count = COUNT(*) BY \`Over time\` = BUCKET(${timeField}, 50, ?_tstart, ?_tend) // ?_tstart and ?_tend take the values of the time picker`, + }, + { + label: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.lastHour.label', + { + defaultMessage: 'Total count vs count last hour', + } + ), + description: i18n.translate( + 'kbn-esql-validation-autocomplete.recommendedQueries.lastHour.description', + { + defaultMessage: 'A more complicated example', + } + ), + queryString: `${fromCommand} + | SORT ${timeField} + | EVAL now = NOW() + | EVAL key = CASE(${timeField} < (now - 1 hour) AND ${timeField} > (now - 2 hour), "Last hour", "Other") + | STATS count = COUNT(*) BY key + | EVAL count_last_hour = CASE(key == "Last hour", count), count_rest = CASE(key == "Other", count) + | EVAL total_visits = TO_DOUBLE(COALESCE(count_last_hour, 0::LONG) + COALESCE(count_rest, 0::LONG)) + | STATS count_last_hour = SUM(count_last_hour), total_visits = SUM(total_visits)`, + }, + ] + : []), + ]; + return queries; +}; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts index 40b688265f3fe3..030bff4da181c7 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts @@ -80,3 +80,9 @@ export interface EditorContext { */ triggerKind: number; } + +export type GetFieldsByTypeFn = ( + type: string | string[], + ignored?: string[], + options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean } +) => Promise; diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 979e718fb41747..e02024968306b6 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -173,6 +173,7 @@ export const commandDefinitions: CommandDefinition[] = [ examples: ['from logs', 'from logs-*', 'from logs_*, events-*'], options: [metadataOption], modes: [], + hasRecommendedQueries: true, signature: { multipleParams: true, params: [{ name: 'index', type: 'source', wildcards: true }], diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index a6e297771cebed..2b1bd618449c35 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -204,6 +204,7 @@ export interface CommandDefinition extends CommandBaseDefinition { examples: string[]; validate?: (option: ESQLCommand) => ESQLMessage[]; modes: CommandModeDefinition[]; + hasRecommendedQueries?: boolean; } export interface Literals { diff --git a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.test.tsx b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.test.tsx index 9a263aa79510dd..2a44f1957d2665 100644 --- a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.test.tsx @@ -11,18 +11,20 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { stubIndexPattern } from '@kbn/data-plugin/public/stubs'; import { coreMock } from '@kbn/core/public/mocks'; +import type { DataView } from '@kbn/data-views-plugin/common'; import { ESQLMenuPopover } from './esql_menu_popover'; describe('ESQLMenuPopover', () => { - const renderESQLPopover = () => { + const renderESQLPopover = (adHocDataview?: DataView) => { const startMock = coreMock.createStart(); const services = { docLinks: startMock.docLinks, }; return render( - {' '} + ); }; @@ -37,8 +39,14 @@ describe('ESQLMenuPopover', () => { expect(screen.getByTestId('esql-menu-button')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button')); expect(screen.getByTestId('esql-quick-reference')).toBeInTheDocument(); - expect(screen.getByTestId('esql-examples')).toBeInTheDocument(); + expect(screen.queryByTestId('esql-recommended-queries')).not.toBeInTheDocument(); expect(screen.getByTestId('esql-about')).toBeInTheDocument(); expect(screen.getByTestId('esql-feedback')).toBeInTheDocument(); }); + + it('should have recommended queries if a dataview is passed', async () => { + renderESQLPopover(stubIndexPattern); + await userEvent.click(screen.getByRole('button')); + expect(screen.queryByTestId('esql-recommended-queries')).toBeInTheDocument(); + }); }); diff --git a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx index cfffa8ae7f83a9..71e54c3376abb6 100644 --- a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx +++ b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx @@ -11,23 +11,28 @@ import React, { useMemo, useState, useCallback } from 'react'; import { EuiPopover, EuiButton, - EuiContextMenuPanel, - type EuiContextMenuPanelProps, + type EuiContextMenuPanelDescriptor, EuiContextMenuItem, - EuiHorizontalRule, + EuiContextMenu, } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; +import type { DataView } from '@kbn/data-views-plugin/public'; import { FEEDBACK_LINK } from '@kbn/esql-utils'; +import { getRecommendedQueries } from '@kbn/esql-validation-autocomplete'; import { LanguageDocumentationFlyout } from '@kbn/language-documentation'; import type { IUnifiedSearchPluginServices } from '../types'; export interface ESQLMenuPopoverProps { onESQLDocsFlyoutVisibilityChanged?: (isOpen: boolean) => void; + adHocDataview?: DataView | string; + onESQLQuerySubmit?: (query: string) => void; } export const ESQLMenuPopover: React.FC = ({ onESQLDocsFlyoutVisibilityChanged, + adHocDataview, + onESQLQuerySubmit, }) => { const kibana = useKibana(); @@ -48,63 +53,115 @@ export const ESQLMenuPopover: React.FC = ({ [setIsLanguageComponentOpen, onESQLDocsFlyoutVisibilityChanged] ); - const esqlPanelItems = useMemo(() => { - const panelItems: EuiContextMenuPanelProps['items'] = []; - panelItems.push( - toggleLanguageComponent()} - > - {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', { - defaultMessage: 'Quick Reference', - })} - , - setIsESQLMenuPopoverOpen(false)} - > - {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { - defaultMessage: 'Documentation', - })} - , - setIsESQLMenuPopoverOpen(false)} - > - {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { - defaultMessage: 'Example queries', - })} - , - , - setIsESQLMenuPopoverOpen(false)} - > - {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { - defaultMessage: 'Submit feedback', - })} - - ); - return panelItems; - }, [ - docLinks.links.query.queryESQL, - docLinks.links.query.queryESQLExamples, - toggleLanguageComponent, - ]); + const esqlContextMenuPanels = useMemo(() => { + const recommendedQueries = []; + if (adHocDataview && typeof adHocDataview !== 'string') { + const queryString = `from ${adHocDataview.name}`; + const timeFieldName = + adHocDataview.timeFieldName ?? adHocDataview.fields?.getByType('date')?.[0]?.name; + + recommendedQueries.push( + ...getRecommendedQueries({ + fromCommand: queryString, + timeField: timeFieldName, + }) + ); + } + const panels = [ + { + id: 0, + items: [ + { + name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', { + defaultMessage: 'Quick Reference', + }), + icon: 'nedocumentationsted', + renderItem: () => ( + toggleLanguageComponent()} + > + {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', { + defaultMessage: 'Quick Reference', + })} + + ), + }, + { + name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { + defaultMessage: 'Documentation', + }), + icon: 'iInCircle', + renderItem: () => ( + setIsESQLMenuPopoverOpen(false)} + > + {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { + defaultMessage: 'Documentation', + })} + + ), + }, + ...(Boolean(recommendedQueries.length) + ? [ + { + name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.exampleQueries', { + defaultMessage: 'Recommended queries', + }), + icon: 'nested', + panel: 1, + 'data-test-subj': 'esql-recommended-queries', + }, + ] + : []), + { + name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.feedback', { + defaultMessage: 'Submit feedback', + }), + icon: 'editorComment', + renderItem: () => ( + setIsESQLMenuPopoverOpen(false)} + > + {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.feedback', { + defaultMessage: 'Submit feedback', + })} + + ), + }, + ], + }, + { + id: 1, + initialFocusedItemIndex: 1, + title: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.exampleQueries', { + defaultMessage: 'Recommended queries', + }), + items: recommendedQueries.map((query) => { + return { + name: query.label, + onClick: () => { + onESQLQuerySubmit?.(query.queryString); + setIsESQLMenuPopoverOpen(false); + }, + }; + }), + }, + ]; + return panels as EuiContextMenuPanelDescriptor[]; + }, [adHocDataview, docLinks.links.query.queryESQL, onESQLQuerySubmit, toggleLanguageComponent]); return ( <> @@ -130,7 +187,7 @@ export const ESQLMenuPopover: React.FC = ({ panelPaddingSize="s" display="block" > - + { + onSubmit({ + query: { esql: queryString } as QT, + dateRange: dateRangeRef.current, + }); + }} + adHocDataview={props.indexPatterns?.[0]} /> )} ({ clear: jest.fn(), }); -const mockIndexPattern = { - id: '1234', - title: 'logstash-*', - fields: [ - { - name: 'response', - type: 'number', - esTypes: ['integer'], - aggregatable: true, - filterable: true, - searchable: true, - }, - ], -} as DataView; - const kqlQuery = { query: 'response:200', language: 'kuery', @@ -155,7 +140,7 @@ describe('SearchBar', () => { it('Should render query bar when no options provided (in reality - timepicker)', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], }) ); @@ -167,7 +152,7 @@ describe('SearchBar', () => { it('Should render filter bar, when required fields are provided', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], showDatePicker: false, showQueryInput: true, showFilterBar: true, @@ -184,7 +169,7 @@ describe('SearchBar', () => { it('Should NOT render filter bar, if disabled', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], showFilterBar: false, filters: [], onFiltersUpdated: noop, @@ -200,7 +185,7 @@ describe('SearchBar', () => { it('Should render query bar, when required fields are provided', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', onQuerySubmit: noop, query: kqlQuery, @@ -215,7 +200,7 @@ describe('SearchBar', () => { it('Should NOT render the input query input, if disabled', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', onQuerySubmit: noop, query: kqlQuery, @@ -231,7 +216,7 @@ describe('SearchBar', () => { it('Should NOT render the query menu button, if disabled', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', onQuerySubmit: noop, query: kqlQuery, @@ -245,7 +230,7 @@ describe('SearchBar', () => { it('Should render query bar and filter bar', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', showQueryInput: true, onQuerySubmit: noop, @@ -264,7 +249,7 @@ describe('SearchBar', () => { it('Should NOT render the input query input, for es|ql query', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', onQuerySubmit: noop, query: esqlQuery, @@ -277,7 +262,7 @@ describe('SearchBar', () => { it('Should render in isDisabled state', () => { const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', onQuerySubmit: noop, isDisabled: true, @@ -316,7 +301,7 @@ describe('SearchBar', () => { const mockedOnQuerySubmit = jest.fn(); const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', onQuerySubmit: mockedOnQuerySubmit, query: kqlQuery, @@ -344,7 +329,7 @@ describe('SearchBar', () => { const mockedOnQuerySubmit = jest.fn(); const component = mount( wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPattern], screenTitle: 'test screen', onQuerySubmit: mockedOnQuerySubmit, query: kqlQuery, diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index c19bfee7781221..f5d2e6ff53c7a6 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -45,6 +45,7 @@ "@kbn/react-kibana-context-render", "@kbn/data-view-utils", "@kbn/esql-utils", + "@kbn/esql-validation-autocomplete", "@kbn/react-kibana-mount", "@kbn/field-utils", "@kbn/language-documentation" From 2873cbca2008b68486bb8439a3035f759be0ebce Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 8 Oct 2024 08:52:51 -0600 Subject: [PATCH 13/50] [Gemini Connector] Bump Default model to `gemini-1.5-pro-002` (#195320) --- docs/management/connectors/action-types/gemini.asciidoc | 2 +- docs/settings/alert-action-settings.asciidoc | 2 +- .../actions/docs/openapi/components/schemas/gemini_config.yaml | 2 +- .../__snapshots__/connector_types.test.ts.snap | 2 +- x-pack/plugins/stack_connectors/common/gemini/constants.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/management/connectors/action-types/gemini.asciidoc b/docs/management/connectors/action-types/gemini.asciidoc index 2371015975d92e..b1d892ff87cc3e 100644 --- a/docs/management/connectors/action-types/gemini.asciidoc +++ b/docs/management/connectors/action-types/gemini.asciidoc @@ -31,7 +31,7 @@ Name:: The name of the connector. API URL:: The {gemini} request URL. Project ID:: The project which has Vertex AI endpoint enabled. Region:: The GCP region where the Vertex AI endpoint enabled. -Default model:: The GAI model for {gemini} to use. Current support is for the Google Gemini models, defaulting to gemini-1.5-pro-001. The model can be set on a per request basis by including a "model" parameter alongside the request body. +Default model:: The GAI model for {gemini} to use. Current support is for the Google Gemini models, defaulting to gemini-1.5-pro-002. The model can be set on a per request basis by including a "model" parameter alongside the request body. Credentials JSON:: The GCP service account JSON file for authentication. [float] diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 464af8ed8d05cf..23cb7ed59e21d7 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -347,7 +347,7 @@ The default model to use for requests, which varies by connector: + -- * For an <>, current support is for the Anthropic Claude models. Defaults to `anthropic.claude-3-5-sonnet-20240620-v1:0`. -* For a <>, current support is for the Gemini models. Defaults to `gemini-1.5-pro-001`. +* For a <>, current support is for the Gemini models. Defaults to `gemini-1.5-pro-002`. * For a <>, it is optional and applicable only when `xpack.actions.preconfigured..config.apiProvider` is `OpenAI`. -- diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_config.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_config.yaml index ed5996dc1c4231..fc3b583f2bfe41 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_config.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_config.yaml @@ -12,7 +12,7 @@ properties: defaultModel: type: string description: The generative artificial intelligence model for Google Gemini to use. - default: gemini-1.5-pro-001 + default: gemini-1.5-pro-002 gcpRegion: type: string description: The GCP region where the Vertex AI endpoint enabled. diff --git a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index 7fab352b7fc7f3..94bc911557c218 100644 --- a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -5297,7 +5297,7 @@ Object { }, "defaultModel": Object { "flags": Object { - "default": "gemini-1.5-pro-001", + "default": "gemini-1.5-pro-002", "error": [Function], "presence": "optional", }, diff --git a/x-pack/plugins/stack_connectors/common/gemini/constants.ts b/x-pack/plugins/stack_connectors/common/gemini/constants.ts index 16bdfb2d91766a..e8d1d1702db2b4 100644 --- a/x-pack/plugins/stack_connectors/common/gemini/constants.ts +++ b/x-pack/plugins/stack_connectors/common/gemini/constants.ts @@ -26,5 +26,5 @@ export enum SUB_ACTION { export const DEFAULT_TOKEN_LIMIT = 8192; export const DEFAULT_TIMEOUT_MS = 60000; export const DEFAULT_GCP_REGION = 'us-central1'; -export const DEFAULT_GEMINI_MODEL = 'gemini-1.5-pro-001'; +export const DEFAULT_GEMINI_MODEL = 'gemini-1.5-pro-002'; export const DEFAULT_GEMINI_URL = `https://us-central1-aiplatform.googleapis.com` as const; From 9f2208db1a83889c76fd2413a0a133a858f4891e Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 8 Oct 2024 09:15:33 -0600 Subject: [PATCH 14/50] [Presentation Util] Cleanup services (#194201) Closes https://github.com/elastic/kibana/issues/167440 ## Summary This PR refactors the `PresentationUtil` services to no longer use its own `PluginServiceProvider`. In doing this, it removes the `PresentationUtil` context provider, since it is no longer necessary. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../components/control_panel.test.tsx | 26 ++-- .../internal_dashboard_top_nav.tsx | 11 +- .../dashboard/public/services/mocks.ts | 2 +- .../public/get_table_list.tsx | 3 +- .../event_annotation_listing/public/plugin.ts | 1 - src/plugins/links/public/mocks.ts | 2 +- .../dashboard_picker/dashboard_picker.tsx | 10 +- .../data_view_picker.stories.tsx | 20 +-- .../floating_actions/floating_actions.tsx | 13 +- .../components/labs/environment_switch.tsx | 10 +- .../components/labs/labs_beaker_button.tsx | 10 +- .../public/components/labs/labs_flyout.tsx | 35 +++--- .../labs/project_list_item.stories.tsx | 4 +- .../saved_object_save_modal_dashboard.tsx | 11 +- ..._save_modal_dashboard_selector.stories.tsx | 3 +- src/plugins/presentation_util/public/index.ts | 7 +- src/plugins/presentation_util/public/mocks.ts | 19 +-- .../presentation_util/public/plugin.ts | 12 +- .../capabilities/capabilities.story.ts | 32 ----- .../capabilities/capabilities.stub.ts | 20 --- .../capabilities/capabilities_service.ts | 28 ----- .../public/services/capabilities/types.ts | 15 --- .../content_management.stub.ts | 26 ---- .../content_management_service.ts | 25 ---- .../services/content_management/types.ts | 14 --- .../services/data_views/data_views.story.ts | 30 ----- .../services/data_views/data_views_service.ts | 29 ----- .../public/services/data_views/types.ts | 16 --- .../public/services/index.ts | 12 -- .../public/services/kibana_services.ts | 45 +++++++ .../public/services/labs/labs.story.ts | 64 ---------- .../public/services/labs/labs.stub.ts | 92 -------------- .../public/services/labs/types.ts | 85 ------------- .../public/services/mocks.ts | 43 +++++++ .../public/services/plugin_services.story.ts | 41 ------ .../public/services/plugin_services.stub.ts | 38 ------ .../public/services/plugin_services.ts | 42 ------- ...ervice.ts => presentation_labs_service.ts} | 119 +++++++++++++----- .../public/services/types.ts | 24 ---- .../public/services/ui_actions/types.ts | 14 --- .../services/ui_actions/ui_actions.stub.ts | 20 --- .../services/ui_actions/ui_actions_service.ts | 26 ---- src/plugins/presentation_util/public/types.ts | 11 +- .../utils/get_presentation_capabilities.ts | 28 +++++ .../presentation_util/storybook/decorator.tsx | 15 +-- .../presentation_util/storybook/main.ts | 8 +- .../presentation_util/storybook/preview.tsx | 5 + src/plugins/presentation_util/tsconfig.json | 1 + src/plugins/visualizations/public/mocks.ts | 2 +- .../public/visualize_app/index.tsx | 20 ++- .../utils/get_top_nav_config.tsx | 2 +- .../change_point_detection_root.tsx | 55 ++++---- .../public/hooks/use_aiops_app_context.ts | 2 - x-pack/plugins/canvas/public/application.tsx | 9 +- .../decorators/services_decorator.tsx | 9 +- .../lens/public/app_plugin/mounter.tsx | 39 +++--- .../app_plugin/shared/saved_modal_lazy.tsx | 6 +- .../lens/public/mocks/services_mock.tsx | 2 +- x-pack/plugins/lens/public/plugin.ts | 4 - x-pack/plugins/maps/public/kibana_services.ts | 1 - .../public/routes/map_page/top_nav_config.tsx | 4 +- .../aiops/change_point_detection.tsx | 1 - .../routing/routes/explorer/state_manager.tsx | 43 +++---- .../timeseriesexplorer_page.tsx | 5 +- .../public/application/index.tsx | 74 +++++------ .../slo/public/application.tsx | 77 ++++++------ .../public/apps/synthetics/synthetics_app.tsx | 42 +++---- 67 files changed, 471 insertions(+), 1093 deletions(-) delete mode 100644 src/plugins/presentation_util/public/services/capabilities/capabilities.story.ts delete mode 100644 src/plugins/presentation_util/public/services/capabilities/capabilities.stub.ts delete mode 100644 src/plugins/presentation_util/public/services/capabilities/capabilities_service.ts delete mode 100644 src/plugins/presentation_util/public/services/capabilities/types.ts delete mode 100644 src/plugins/presentation_util/public/services/content_management/content_management.stub.ts delete mode 100644 src/plugins/presentation_util/public/services/content_management/content_management_service.ts delete mode 100644 src/plugins/presentation_util/public/services/content_management/types.ts delete mode 100644 src/plugins/presentation_util/public/services/data_views/data_views.story.ts delete mode 100644 src/plugins/presentation_util/public/services/data_views/data_views_service.ts delete mode 100644 src/plugins/presentation_util/public/services/data_views/types.ts delete mode 100644 src/plugins/presentation_util/public/services/index.ts create mode 100644 src/plugins/presentation_util/public/services/kibana_services.ts delete mode 100644 src/plugins/presentation_util/public/services/labs/labs.story.ts delete mode 100644 src/plugins/presentation_util/public/services/labs/labs.stub.ts delete mode 100644 src/plugins/presentation_util/public/services/labs/types.ts create mode 100644 src/plugins/presentation_util/public/services/mocks.ts delete mode 100644 src/plugins/presentation_util/public/services/plugin_services.story.ts delete mode 100644 src/plugins/presentation_util/public/services/plugin_services.stub.ts delete mode 100644 src/plugins/presentation_util/public/services/plugin_services.ts rename src/plugins/presentation_util/public/services/{labs/labs_service.ts => presentation_labs_service.ts} (58%) delete mode 100644 src/plugins/presentation_util/public/services/types.ts delete mode 100644 src/plugins/presentation_util/public/services/ui_actions/types.ts delete mode 100644 src/plugins/presentation_util/public/services/ui_actions/ui_actions.stub.ts delete mode 100644 src/plugins/presentation_util/public/services/ui_actions/ui_actions_service.ts create mode 100644 src/plugins/presentation_util/public/utils/get_presentation_capabilities.ts diff --git a/src/plugins/controls/public/control_group/components/control_panel.test.tsx b/src/plugins/controls/public/control_group/components/control_panel.test.tsx index 116e268afe208f..0f6d8b07b324af 100644 --- a/src/plugins/controls/public/control_group/components/control_panel.test.tsx +++ b/src/plugins/controls/public/control_group/components/control_panel.test.tsx @@ -10,12 +10,13 @@ import React, { useImperativeHandle } from 'react'; import { BehaviorSubject } from 'rxjs'; -import { pluginServices as presentationUtilPluginServices } from '@kbn/presentation-util-plugin/public/services'; -import { registry as presentationUtilServicesRegistry } from '@kbn/presentation-util-plugin/public/services/plugin_services.story'; +import { setMockedPresentationUtilServices } from '@kbn/presentation-util-plugin/public/mocks'; +import { uiActionsService } from '@kbn/presentation-util-plugin/public/services/kibana_services'; import { render, waitFor } from '@testing-library/react'; import type { ControlLabelPosition, ControlWidth } from '../../../common'; import { ControlPanel } from './control_panel'; +import { Action } from '@kbn/ui-actions-plugin/public'; describe('render', () => { let mockApi = {}; @@ -27,19 +28,14 @@ describe('render', () => { }) as any; beforeAll(() => { - presentationUtilServicesRegistry.start({}); - presentationUtilPluginServices.setRegistry(presentationUtilServicesRegistry); - presentationUtilPluginServices.getServices().uiActions.getTriggerCompatibleActions = jest - .fn() - .mockImplementation(() => { - return [ - { - isCompatible: jest.fn().mockResolvedValue(true), - id: 'testAction', - MenuItem: () =>
test1
, - }, - ]; - }); + setMockedPresentationUtilServices(); + jest.spyOn(uiActionsService, 'getTriggerCompatibleActions').mockResolvedValue([ + { + isCompatible: jest.fn().mockResolvedValue(true), + id: 'testAction', + MenuItem: () =>
test1
, + }, + ] as unknown as Action[]); }); beforeEach(() => { diff --git a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index bcfcf2e98dddfb..bdbb506dfc7138 100644 --- a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -25,11 +25,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { getManagedContentBadge } from '@kbn/managed-content-badge'; import { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import { - LazyLabsFlyout, - getContextProvider as getPresentationUtilContextProvider, - withSuspense, -} from '@kbn/presentation-util-plugin/public'; +import { LazyLabsFlyout, withSuspense } from '@kbn/presentation-util-plugin/public'; import { UI_SETTINGS } from '../../common'; import { useDashboardApi } from '../dashboard_api/use_dashboard_api'; @@ -88,7 +84,6 @@ export function InternalDashboardTopNav({ const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext(); const dashboardApi = useDashboardApi(); - const PresentationUtilContextProvider = getPresentationUtilContextProvider(); const [ allDataViews, @@ -405,9 +400,7 @@ export function InternalDashboardTopNav({ onSavedQueryIdChange={setSavedQueryId} /> {viewMode !== 'print' && isLabsEnabled && isLabsShown ? ( - - setIsLabsShown(false)} /> - + setIsLabsShown(false)} /> ) : null} {viewMode === 'edit' ? : null} {showBorderBottom && } diff --git a/src/plugins/dashboard/public/services/mocks.ts b/src/plugins/dashboard/public/services/mocks.ts index 61132c2fc264ef..255098ecd81966 100644 --- a/src/plugins/dashboard/public/services/mocks.ts +++ b/src/plugins/dashboard/public/services/mocks.ts @@ -60,7 +60,7 @@ export const setStubKibanaServices = () => { navigation: navigationPluginMock.createStartContract(), noDataPage: noDataPagePublicMock.createStart(), observabilityAIAssistant: observabilityAIAssistantPluginMock.createStartContract(), - presentationUtil: presentationUtilPluginMock.createStartContract(core), + presentationUtil: presentationUtilPluginMock.createStartContract(), savedObjectsManagement: savedObjectsManagementPluginMock.createStartContract(), savedObjectsTaggingOss: savedObjectTaggingOssPluginMock.createStart(), screenshotMode: screenshotModePluginMock.createStartContract(), diff --git a/src/plugins/event_annotation_listing/public/get_table_list.tsx b/src/plugins/event_annotation_listing/public/get_table_list.tsx index da150bf63bac5b..45ed046340fac4 100644 --- a/src/plugins/event_annotation_listing/public/get_table_list.tsx +++ b/src/plugins/event_annotation_listing/public/get_table_list.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { FC } from 'react'; +import React from 'react'; import { FormattedRelative } from '@kbn/i18n-react'; import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table'; import { type TableListTabParentProps } from '@kbn/content-management-tabbed-table-list-view'; @@ -25,7 +25,6 @@ export interface EventAnnotationListingPageServices { core: CoreStart; savedObjectsTagging: SavedObjectsTaggingApi; eventAnnotationService: EventAnnotationServiceType; - PresentationUtilContextProvider: FC; dataViews: DataView[]; createDataView: (spec: DataViewSpec) => Promise; queryInputServices: QueryInputServices; diff --git a/src/plugins/event_annotation_listing/public/plugin.ts b/src/plugins/event_annotation_listing/public/plugin.ts index 4f8f9454e4c330..d32a26f19022cc 100644 --- a/src/plugins/event_annotation_listing/public/plugin.ts +++ b/src/plugins/event_annotation_listing/public/plugin.ts @@ -74,7 +74,6 @@ export class EventAnnotationListingPlugin LensEmbeddableComponent: pluginsStart.lens.EmbeddableComponent, savedObjectsTagging: pluginsStart.savedObjectsTagging, eventAnnotationService, - PresentationUtilContextProvider: pluginsStart.presentationUtil.ContextProvider, dataViews, createDataView: pluginsStart.dataViews.create.bind(pluginsStart.dataViews), sessionService: pluginsStart.data.search.session, diff --git a/src/plugins/links/public/mocks.ts b/src/plugins/links/public/mocks.ts index 21e900339b41d3..dc4f5d57d479f5 100644 --- a/src/plugins/links/public/mocks.ts +++ b/src/plugins/links/public/mocks.ts @@ -40,7 +40,7 @@ export const setStubKibanaServices = () => { dashboard: dashboardPluginMock.createStartContract(), embeddable: embeddablePluginMock.createStartContract(), contentManagement: contentManagementMock.createStartContract(), - presentationUtil: presentationUtilPluginMock.createStartContract(core), + presentationUtil: presentationUtilPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), }); }; diff --git a/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.tsx index aaa9dc957e491d..1f1f494b2d5319 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import { ToolbarButton } from '@kbn/shared-ux-button-toolbar'; import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; -import { pluginServices } from '../../services'; +import { contentManagementService } from '../../services/kibana_services'; export interface DashboardPickerProps { onChange: (dashboard: { name: string; id: string } | null) => void; @@ -53,10 +53,6 @@ export function DashboardPicker({ isDisabled, onChange, idsToOmit }: DashboardPi const [selectedDashboard, setSelectedDashboard] = useState(null); - const { - contentManagement: { client: cmClient }, - } = pluginServices.getServices(); - /** * Debounce the query to avoid many calls to content management. */ @@ -77,7 +73,7 @@ export function DashboardPicker({ isDisabled, onChange, idsToOmit }: DashboardPi (async () => { setIsLoading(true); - const response = await cmClient.mSearch({ + const response = await contentManagementService.client.mSearch({ contentTypes: [{ contentTypeId: 'dashboard' }], query: { text: debouncedQuery ? `${debouncedQuery}*` : undefined, @@ -95,7 +91,7 @@ export function DashboardPicker({ isDisabled, onChange, idsToOmit }: DashboardPi return () => { canceled = true; }; - }, [debouncedQuery, cmClient]); + }, [debouncedQuery]); /** * Format items with dashboard hits and selected option diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx index 756813b7e201fd..2e3a6140365de0 100644 --- a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.stories.tsx @@ -9,12 +9,10 @@ import React, { useState } from 'react'; -import useMount from 'react-use/lib/useMount'; import { DataView, DataViewListItem } from '@kbn/data-views-plugin/common'; +import useMount from 'react-use/lib/useMount'; import { DataViewPicker } from './data_view_picker'; -import { injectStorybookDataView } from '../../services/data_views/data_views.story'; -import { storybookFlightsDataView } from '../../mocks'; -import { pluginServices, registry, StorybookParams } from '../../services/plugin_services.story'; +import { dataViewsService } from '../../services/kibana_services'; export default { component: DataViewPicker, @@ -22,27 +20,19 @@ export default { argTypes: {}, }; -injectStorybookDataView(storybookFlightsDataView); - -export function Example({}: {} & StorybookParams) { - pluginServices.setRegistry(registry.start({})); - - const { - dataViews: { getIdsWithTitle, get }, - } = pluginServices.getServices(); - +export function Example() { const [dataViews, setDataViews] = useState(); const [dataView, setDataView] = useState(undefined); useMount(() => { (async () => { - const listItems = await getIdsWithTitle(); + const listItems = await dataViewsService.getIdsWithTitle(); setDataViews(listItems); })(); }); const onChange = (newId: string) => { - get(newId).then((newDataView) => { + dataViewsService.get(newId).then((newDataView) => { setDataView(newDataView); }); }; diff --git a/src/plugins/presentation_util/public/components/floating_actions/floating_actions.tsx b/src/plugins/presentation_util/public/components/floating_actions/floating_actions.tsx index 8eb9ff4f2cef87..3c03e65714908d 100644 --- a/src/plugins/presentation_util/public/components/floating_actions/floating_actions.tsx +++ b/src/plugins/presentation_util/public/components/floating_actions/floating_actions.tsx @@ -12,15 +12,15 @@ import React, { FC, ReactElement, useEffect, useState } from 'react'; import { v4 } from 'uuid'; import { - panelHoverTrigger, PANEL_HOVER_TRIGGER, + panelHoverTrigger, type EmbeddableInput, type ViewMode, } from '@kbn/embeddable-plugin/public'; import { apiHasUniqueId } from '@kbn/presentation-publishing'; import { Action } from '@kbn/ui-actions-plugin/public'; -import { pluginServices } from '../../services'; +import { uiActionsService } from '../../services/kibana_services'; import './floating_actions.scss'; export interface FloatingActionsProps { @@ -41,9 +41,6 @@ export const FloatingActions: FC = ({ className = '', disabledActions, }) => { - const { - uiActions: { getTriggerCompatibleActions }, - } = pluginServices.getServices(); const [floatingActions, setFloatingActions] = useState(undefined); useEffect(() => { @@ -55,7 +52,9 @@ export const FloatingActions: FC = ({ embeddable: api, trigger: panelHoverTrigger, }; - const actions = (await getTriggerCompatibleActions(PANEL_HOVER_TRIGGER, context)) + const actions = ( + await uiActionsService.getTriggerCompatibleActions(PANEL_HOVER_TRIGGER, context) + ) .filter((action): action is Action & { MenuItem: React.FC<{ context: unknown }> } => { return action.MenuItem !== undefined && (disabledActions ?? []).indexOf(action.id) === -1; }) @@ -82,7 +81,7 @@ export const FloatingActions: FC = ({ }; getActions(); - }, [api, getTriggerCompatibleActions, viewMode, disabledActions]); + }, [api, viewMode, disabledActions]); return (
diff --git a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx index 83ac5363687871..94e71fe432fc57 100644 --- a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx +++ b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -17,9 +17,9 @@ import { EuiScreenReaderOnly, } from '@elastic/eui'; -import { pluginServices } from '../../services'; import { EnvironmentName } from '../../../common/labs'; import { LabsStrings } from '../../i18n'; +import { getPresentationCapabilities } from '../../utils/get_presentation_capabilities'; const { Switch: strings } = LabsStrings.Components; @@ -37,9 +37,11 @@ export interface Props { } export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => { - const { capabilities } = pluginServices.getHooks(); + const { canSetAdvancedSettings } = useMemo(() => { + return getPresentationCapabilities(); + }, []); - const canSet = env === 'kibana' ? capabilities.useService().canSetAdvancedSettings() : true; + const canSet = env === 'kibana' ? canSetAdvancedSettings : true; return ( diff --git a/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx index ca20f973344c69..1d454050963d99 100644 --- a/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx +++ b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx @@ -7,20 +7,20 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { EuiButton, EuiIcon, EuiNotificationBadge, EuiButtonProps } from '@elastic/eui'; -import { pluginServices } from '../../services'; import { LabsFlyout, Props as FlyoutProps } from './labs_flyout'; +import { getPresentationLabsService } from '../../services/presentation_labs_service'; export type Props = EuiButtonProps & Pick; export const LabsBeakerButton = ({ solutions, ...props }: Props) => { - const { labs: labsService } = pluginServices.getHooks(); - const { getProjects } = labsService.useService(); + const labsService = useMemo(() => getPresentationLabsService(), []); + const [isOpen, setIsOpen] = useState(false); - const projects = getProjects(); + const projects = labsService.getProjects(); const [overrideCount, onEnabledCountChange] = useState( Object.values(projects).filter((project) => project.status.isOverride).length diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx index 2946adb8b74884..d7ec712c3eeded 100644 --- a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -7,26 +7,26 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { ReactNode, useRef, useState, useEffect } from 'react'; import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, EuiFlyout, - EuiTitle, - EuiSpacer, - EuiText, EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, - EuiButton, - EuiButtonEmpty, - EuiFlexItem, - EuiFlexGroup, EuiIcon, + EuiSpacer, + EuiText, + EuiTitle, } from '@elastic/eui'; +import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; -import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; -import { pluginServices } from '../../services'; +import { EnvironmentName, Project, ProjectID, ProjectStatus, SolutionName } from '../../../common'; import { LabsStrings } from '../../i18n'; +import { getPresentationLabsService } from '../../services/presentation_labs_service'; import { ProjectList } from './project_list'; const { Flyout: strings } = LabsStrings.Components; @@ -56,12 +56,11 @@ export const getOverridenCount = (projects: Record) => export const LabsFlyout = (props: Props) => { const { solutions, onEnabledCountChange = () => {}, onClose } = props; - const { labs: labsService } = pluginServices.getHooks(); - const { getProjects, setProjectStatus, reset } = labsService.useService(); + const labsService = useMemo(() => getPresentationLabsService(), []); - const [projects, setProjects] = useState(getProjects()); + const [projects, setProjects] = useState(labsService.getProjects()); const [overrideCount, setOverrideCount] = useState(getOverridenCount(projects)); - const initialStatus = useRef(getProjects()); + const initialStatus = useRef(labsService.getProjects()); const isChanged = hasStatusChanged(initialStatus.current, projects); @@ -74,8 +73,8 @@ export const LabsFlyout = (props: Props) => { }, [onEnabledCountChange, overrideCount]); const onStatusChange = (id: ProjectID, env: EnvironmentName, enabled: boolean) => { - setProjectStatus(id, env, enabled); - setProjects(getProjects()); + labsService.setProjectStatus(id, env, enabled); + setProjects(labsService.getProjects()); }; let footer: ReactNode = null; @@ -83,8 +82,8 @@ export const LabsFlyout = (props: Props) => { const resetButton = ( { - reset(); - setProjects(getProjects()); + labsService.reset(); + setProjects(labsService.getProjects()); }} isDisabled={!overrideCount} > diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx index 94feb75d3623d4..3a157a972647aa 100644 --- a/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx @@ -7,15 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; import { action } from '@storybook/addon-actions'; import { mapValues } from 'lodash'; +import React from 'react'; import { EnvironmentStatus, ProjectConfig, ProjectID, ProjectStatus } from '../../../common'; -import { applyProjectStatus } from '../../services/labs/types'; import { ProjectListItem, Props } from './project_list_item'; import { projects as projectConfigs } from '../../../common'; +import { applyProjectStatus } from '../../services/presentation_labs_service'; import { ProjectList } from './project_list'; export default { diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 9bb8f0471375ae..e2e87649d42515 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -17,20 +17,21 @@ import { type SaveModalState, } from '@kbn/saved-objects-plugin/public'; -import { pluginServices } from '../services'; import { SaveModalDashboardProps } from './types'; import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; +import { getPresentationCapabilities } from '../utils/get_presentation_capabilities'; function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; const { id: documentId } = documentInfo; const initialCopyOnSave = !Boolean(documentId); - const { capabilities } = pluginServices.getHooks(); - const { canAccessDashboards, canCreateNewDashboards } = capabilities.useService(); + const { canAccessDashboards, canCreateNewDashboards } = useMemo(() => { + return getPresentationCapabilities(); + }, []); // Disable the dashboard options if the user can't access dashboards or if they're read-only - const disableDashboardOptions = !canAccessDashboards() || !canCreateNewDashboards(); + const disableDashboardOptions = !canAccessDashboards || !canCreateNewDashboards; const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>( documentId || disableDashboardOptions ? null : 'existing' diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx index 336ff500ba9e14..95b03d66b65fe3 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx @@ -10,7 +10,6 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; -import { StorybookParams } from '../services/plugin_services.story'; import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; export default { @@ -49,7 +48,7 @@ export function Example({ copyOnSave: boolean; hasDocumentId: boolean; canSaveVisualizations: boolean; -} & StorybookParams) { +}) { const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing'); const [isAddToLibrarySelected, setAddToLibrary] = useState(false); diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index 74335d385e5f4a..16f33d237364a9 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -9,9 +9,8 @@ import { ExpressionFunction } from '@kbn/expressions-plugin/common'; import { PresentationUtilPlugin } from './plugin'; -import { pluginServices } from './services'; -export type { PresentationCapabilitiesService, PresentationLabsService } from './services'; +export type { PresentationLabsService } from './services/presentation_labs_service'; export type { KibanaPluginServiceFactory, @@ -70,7 +69,3 @@ export const registerExpressionsLanguage = async (expressionFunctions: Expressio export function plugin() { return new PresentationUtilPlugin(); } - -export const useLabs = () => (() => pluginServices.getHooks().labs.useService())(); - -export const getContextProvider = () => pluginServices.getContextProvider(); diff --git a/src/plugins/presentation_util/public/mocks.ts b/src/plugins/presentation_util/public/mocks.ts index e5a858375e98dc..e9eea6790d3df7 100644 --- a/src/plugins/presentation_util/public/mocks.ts +++ b/src/plugins/presentation_util/public/mocks.ts @@ -7,20 +7,21 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreStart } from '@kbn/core/public'; import { PresentationUtilPluginStart } from './types'; -import { pluginServices } from './services'; -import { registry as stubRegistry } from './services/plugin_services.story'; import { ReduxToolsPackage, registerExpressionsLanguage } from '.'; import { createReduxEmbeddableTools } from './redux_tools/redux_embeddables/create_redux_embeddable_tools'; import { createReduxTools } from './redux_tools/create_redux_tools'; +import { setStubKibanaServices } from './services/mocks'; -const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart => { - pluginServices.setRegistry(stubRegistry.start({})); - +const createStartContract = (): PresentationUtilPluginStart => { const startContract: PresentationUtilPluginStart = { - ContextProvider: pluginServices.getContextProvider(), - labsService: pluginServices.getServices().labs, + labsService: { + getProjects: jest.fn(), + getProject: jest.fn(), + isProjectEnabled: jest.fn(), + reset: jest.fn(), + setProjectStatus: jest.fn(), + }, registerExpressionsLanguage, }; return startContract; @@ -40,5 +41,5 @@ export const mockedReduxEmbeddablePackage: ReduxToolsPackage = { export * from './__stories__/fixtures/flights'; export const setMockedPresentationUtilServices = () => { - pluginServices.setRegistry(stubRegistry.start({})); + setStubKibanaServices(); }; diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 55294ae0ed6838..e5055c53e2df3e 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -8,15 +8,16 @@ */ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; -import { pluginServices, registry } from './services/plugin_services'; import { - PresentationUtilPluginSetupDeps, - PresentationUtilPluginStartDeps, PresentationUtilPluginSetup, + PresentationUtilPluginSetupDeps, PresentationUtilPluginStart, + PresentationUtilPluginStartDeps, } from './types'; import { registerExpressionsLanguage } from '.'; +import { setKibanaServices } from './services/kibana_services'; +import { getPresentationLabsService } from './services/presentation_labs_service'; export class PresentationUtilPlugin implements @@ -38,11 +39,10 @@ export class PresentationUtilPlugin coreStart: CoreStart, startPlugins: PresentationUtilPluginStartDeps ): PresentationUtilPluginStart { - pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); + setKibanaServices(coreStart, startPlugins); return { - ContextProvider: pluginServices.getContextProvider(), - labsService: pluginServices.getServices().labs, + labsService: getPresentationLabsService(), registerExpressionsLanguage, }; } diff --git a/src/plugins/presentation_util/public/services/capabilities/capabilities.story.ts b/src/plugins/presentation_util/public/services/capabilities/capabilities.story.ts deleted file mode 100644 index 4c8ae3e8936087..00000000000000 --- a/src/plugins/presentation_util/public/services/capabilities/capabilities.story.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '../create'; -import { StorybookParams } from '../plugin_services.story'; -import { PresentationCapabilitiesService } from './types'; - -type CapabilitiesServiceFactory = PluginServiceFactory< - PresentationCapabilitiesService, - StorybookParams ->; - -export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ - canAccessDashboards, - canCreateNewDashboards, - canSaveVisualizations, - canSetAdvancedSettings, -}) => { - const check = (value: boolean = true) => value; - return { - canAccessDashboards: () => check(canAccessDashboards), - canCreateNewDashboards: () => check(canCreateNewDashboards), - canSaveVisualizations: () => check(canSaveVisualizations), - canSetAdvancedSettings: () => check(canSetAdvancedSettings), - }; -}; diff --git a/src/plugins/presentation_util/public/services/capabilities/capabilities.stub.ts b/src/plugins/presentation_util/public/services/capabilities/capabilities.stub.ts deleted file mode 100644 index f46471c8efb541..00000000000000 --- a/src/plugins/presentation_util/public/services/capabilities/capabilities.stub.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from './types'; - -type CapabilitiesServiceFactory = PluginServiceFactory; - -export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({ - canAccessDashboards: () => true, - canCreateNewDashboards: () => true, - canSaveVisualizations: () => true, - canSetAdvancedSettings: () => true, -}); diff --git a/src/plugins/presentation_util/public/services/capabilities/capabilities_service.ts b/src/plugins/presentation_util/public/services/capabilities/capabilities_service.ts deleted file mode 100644 index d286802b171413..00000000000000 --- a/src/plugins/presentation_util/public/services/capabilities/capabilities_service.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PresentationUtilPluginStartDeps } from '../../types'; -import { KibanaPluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from './types'; - -export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< - PresentationCapabilitiesService, - PresentationUtilPluginStartDeps ->; - -export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => { - const { dashboard, visualize, advancedSettings } = coreStart.application.capabilities; - - return { - canAccessDashboards: () => Boolean(dashboard.show), - canCreateNewDashboards: () => Boolean(dashboard.createNew), - canSaveVisualizations: () => Boolean(visualize.save), - canSetAdvancedSettings: () => Boolean(advancedSettings.save), - }; -}; diff --git a/src/plugins/presentation_util/public/services/capabilities/types.ts b/src/plugins/presentation_util/public/services/capabilities/types.ts deleted file mode 100644 index 51d323c0c0d54b..00000000000000 --- a/src/plugins/presentation_util/public/services/capabilities/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export interface PresentationCapabilitiesService { - canAccessDashboards: () => boolean; - canCreateNewDashboards: () => boolean; - canSaveVisualizations: () => boolean; - canSetAdvancedSettings: () => boolean; -} diff --git a/src/plugins/presentation_util/public/services/content_management/content_management.stub.ts b/src/plugins/presentation_util/public/services/content_management/content_management.stub.ts deleted file mode 100644 index c492cce96fc233..00000000000000 --- a/src/plugins/presentation_util/public/services/content_management/content_management.stub.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '../create'; -import { PresentationContentManagementService } from './types'; - -type ContentManagementServiceFactory = PluginServiceFactory; - -export const contentManagementServiceFactory: ContentManagementServiceFactory = () => ({ - client: { - get: jest.fn(), - get$: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - search: jest.fn(), - search$: jest.fn(), - mSearch: jest.fn(), - } as unknown as PresentationContentManagementService['client'], -}); diff --git a/src/plugins/presentation_util/public/services/content_management/content_management_service.ts b/src/plugins/presentation_util/public/services/content_management/content_management_service.ts deleted file mode 100644 index 7a5986c55ba800..00000000000000 --- a/src/plugins/presentation_util/public/services/content_management/content_management_service.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PresentationUtilPluginStartDeps } from '../../types'; -import { PresentationContentManagementService } from './types'; -import { KibanaPluginServiceFactory } from '../create'; - -export type PresentationContentManagementServiceFactory = KibanaPluginServiceFactory< - PresentationContentManagementService, - PresentationUtilPluginStartDeps ->; - -export const contentManagementServiceFactory: PresentationContentManagementServiceFactory = ({ - startPlugins, -}) => { - return { - client: startPlugins.contentManagement.client, - }; -}; diff --git a/src/plugins/presentation_util/public/services/content_management/types.ts b/src/plugins/presentation_util/public/services/content_management/types.ts deleted file mode 100644 index f87d45072aa980..00000000000000 --- a/src/plugins/presentation_util/public/services/content_management/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; - -export interface PresentationContentManagementService { - client: ContentManagementPublicStart['client']; -} diff --git a/src/plugins/presentation_util/public/services/data_views/data_views.story.ts b/src/plugins/presentation_util/public/services/data_views/data_views.story.ts deleted file mode 100644 index de82be53bb00ff..00000000000000 --- a/src/plugins/presentation_util/public/services/data_views/data_views.story.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/common'; -import { PluginServiceFactory } from '../create'; -import { PresentationDataViewsService } from './types'; - -export type DataViewsServiceFactory = PluginServiceFactory; - -let currentDataView: DataView; -export const injectStorybookDataView = (dataView: DataView) => (currentDataView = dataView); - -export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({ - get: (() => - new Promise((r) => - setTimeout(() => r(currentDataView), 100) - ) as unknown) as DataViewsPublicPluginStart['get'], - getIdsWithTitle: (() => - new Promise((r) => - setTimeout(() => r([{ id: currentDataView.id, title: currentDataView.title }]), 100) - ) as unknown) as DataViewsPublicPluginStart['getIdsWithTitle'], - getDefaultId: () => Promise.resolve(currentDataView?.id ?? null), -}); diff --git a/src/plugins/presentation_util/public/services/data_views/data_views_service.ts b/src/plugins/presentation_util/public/services/data_views/data_views_service.ts deleted file mode 100644 index 40a47bc14713fe..00000000000000 --- a/src/plugins/presentation_util/public/services/data_views/data_views_service.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PresentationUtilPluginStartDeps } from '../../types'; -import { PresentationDataViewsService } from './types'; -import { KibanaPluginServiceFactory } from '../create'; - -export type DataViewsServiceFactory = KibanaPluginServiceFactory< - PresentationDataViewsService, - PresentationUtilPluginStartDeps ->; - -export const dataViewsServiceFactory: DataViewsServiceFactory = ({ startPlugins }) => { - const { - dataViews: { get, getIdsWithTitle, getDefaultId }, - } = startPlugins; - - return { - get, - getDefaultId, - getIdsWithTitle, - }; -}; diff --git a/src/plugins/presentation_util/public/services/data_views/types.ts b/src/plugins/presentation_util/public/services/data_views/types.ts deleted file mode 100644 index bb0af7c7e607d0..00000000000000 --- a/src/plugins/presentation_util/public/services/data_views/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; - -export interface PresentationDataViewsService { - get: DataViewsPublicPluginStart['get']; - getDefaultId: DataViewsPublicPluginStart['getDefaultId']; - getIdsWithTitle: DataViewsPublicPluginStart['getIdsWithTitle']; -} diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts deleted file mode 100644 index ccfcc112a41f70..00000000000000 --- a/src/plugins/presentation_util/public/services/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { pluginServices } from './plugin_services'; - -export type { PresentationCapabilitiesService, PresentationLabsService } from './types'; diff --git a/src/plugins/presentation_util/public/services/kibana_services.ts b/src/plugins/presentation_util/public/services/kibana_services.ts new file mode 100644 index 00000000000000..1299544623bd5e --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana_services.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; + +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +import { PresentationUtilPluginStartDeps } from '../types'; + +export let coreServices: CoreStart; +export let contentManagementService: ContentManagementPublicStart; +export let dataViewsService: DataViewsPublicPluginStart; +export let uiActionsService: UiActionsPublicStart; + +const servicesReady$ = new BehaviorSubject(false); + +export const setKibanaServices = (kibanaCore: CoreStart, deps: PresentationUtilPluginStartDeps) => { + coreServices = kibanaCore; + contentManagementService = deps.contentManagement; + dataViewsService = deps.dataViews; + uiActionsService = deps.uiActions; + + servicesReady$.next(true); +}; + +export const untilPluginStartServicesReady = () => { + if (servicesReady$.value) return Promise.resolve(); + return new Promise((resolve) => { + const subscription = servicesReady$.subscribe((isInitialized) => { + if (isInitialized) { + subscription.unsubscribe(); + resolve(); + } + }); + }); +}; diff --git a/src/plugins/presentation_util/public/services/labs/labs.story.ts b/src/plugins/presentation_util/public/services/labs/labs.story.ts deleted file mode 100644 index 0ae9fc8b89a04b..00000000000000 --- a/src/plugins/presentation_util/public/services/labs/labs.story.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EnvironmentName, projectIDs, Project } from '../../../common'; -import { PluginServiceFactory } from '../create'; -import { projects, ProjectID, getProjectIDs, SolutionName } from '../../../common'; -import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from './types'; - -export type LabsServiceFactory = PluginServiceFactory; - -export const labsServiceFactory: LabsServiceFactory = () => { - const storage = window.sessionStorage; - - const getProjects = (solutions: SolutionName[] = []) => - projectIDs.reduce((acc, id) => { - const project = getProject(id); - if ( - solutions.length === 0 || - solutions.some((solution) => project.solutions.includes(solution)) - ) { - acc[id] = project; - } - return acc; - }, {} as { [id in ProjectID]: Project }); - - const getProject = (id: ProjectID) => { - const project = projects[id]; - const { isActive } = project; - const status = { - session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), - browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), - kibana: isActive, - }; - return applyProjectStatus(project, status); - }; - - const setProjectStatus = (name: ProjectID, env: EnvironmentName, enabled: boolean) => { - if (env === 'session') { - storage.setItem(name, enabled ? 'enabled' : 'disabled'); - } - }; - - const reset = () => { - // This is normally not ok, but it's our isolated Storybook instance. - storage.clear(); - }; - - const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; - - return { - getProjectIDs, - getProjects, - getProject, - isProjectEnabled, - reset, - setProjectStatus, - }; -}; diff --git a/src/plugins/presentation_util/public/services/labs/labs.stub.ts b/src/plugins/presentation_util/public/services/labs/labs.stub.ts deleted file mode 100644 index 21e44b38f1ac3a..00000000000000 --- a/src/plugins/presentation_util/public/services/labs/labs.stub.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - projects, - projectIDs, - ProjectID, - EnvironmentName, - getProjectIDs, - Project, - SolutionName, -} from '../../../common'; -import { PluginServiceFactory } from '../create'; -import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from './types'; - -export type LabsServiceFactory = PluginServiceFactory; - -type Statuses = { - [id in ProjectID]: { - defaultValue: boolean; - session: boolean | null; - browser: boolean | null; - kibana: boolean; - }; -}; - -export const labsServiceFactory: LabsServiceFactory = () => { - let statuses = {} as Statuses; - - const getProject = (id: ProjectID) => { - const project = projects[id]; - const value = statuses[id]; - const status = { - session: isEnabledByStorageValue(project, 'session', value.session), - browser: isEnabledByStorageValue(project, 'browser', value.browser), - kibana: isEnabledByStorageValue(project, 'kibana', value.kibana), - }; - - return applyProjectStatus(project, status); - }; - - const reset = () => - projectIDs.reduce((acc, id) => { - const project = projects[id]; - const defaultValue = project.isActive; - - acc[id] = { - defaultValue, - session: null, - browser: null, - kibana: defaultValue, - }; - return acc; - }, {} as Statuses); - - statuses = reset(); - - const getProjects = (solutions: SolutionName[] = []) => - projectIDs.reduce((acc, id) => { - const project = getProject(id); - if ( - solutions.length === 0 || - solutions.some((solution) => project.solutions.includes(solution)) - ) { - acc[id] = project; - } - return acc; - }, {} as { [id in ProjectID]: Project }); - - const setProjectStatus = (id: ProjectID, env: EnvironmentName, value: boolean) => { - statuses[id] = { ...statuses[id], [env]: value }; - }; - - const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; - - return { - getProjectIDs, - getProject, - getProjects, - isProjectEnabled, - setProjectStatus, - reset: () => { - statuses = reset(); - }, - }; -}; diff --git a/src/plugins/presentation_util/public/services/labs/types.ts b/src/plugins/presentation_util/public/services/labs/types.ts deleted file mode 100644 index 4c721c84f6a6f4..00000000000000 --- a/src/plugins/presentation_util/public/services/labs/types.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { IUiSettingsClient } from '@kbn/core/public'; -import { - EnvironmentName, - projectIDs, - Project, - ProjectConfig, - ProjectID, - EnvironmentStatus, - environmentNames, - isProjectEnabledByStatus, - SolutionName, -} from '../../../common'; - -export interface PresentationLabsService { - isProjectEnabled: (id: ProjectID) => boolean; - getProjectIDs: () => typeof projectIDs; - getProject: (id: ProjectID) => Project; - getProjects: (solutions?: SolutionName[]) => Record; - setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; - reset: () => void; -} - -export const isEnabledByStorageValue = ( - project: ProjectConfig, - environment: EnvironmentName, - value: string | boolean | null -): boolean => { - const defaultValue = project.isActive; - - if (!project.environments.includes(environment)) { - return defaultValue; - } - - if (value === true || value === false) { - return value; - } - - if (value === 'enabled') { - return true; - } - - if (value === 'disabled') { - return false; - } - - return defaultValue; -}; - -export const setStorageStatus = (storage: Storage, id: ProjectID, enabled: boolean) => - storage.setItem(id, enabled ? 'enabled' : 'disabled'); - -export const applyProjectStatus = (project: ProjectConfig, status: EnvironmentStatus): Project => { - const { isActive, environments } = project; - - environmentNames.forEach((name) => { - if (!environments.includes(name)) { - delete status[name]; - } - }); - - const isEnabled = isProjectEnabledByStatus(isActive, status); - const isOverride = isEnabled !== isActive; - - return { - ...project, - status: { - ...status, - defaultValue: isActive, - isEnabled, - isOverride, - }, - }; -}; - -export const setUISettingsStatus = (client: IUiSettingsClient, id: ProjectID, enabled: boolean) => - client.set(id, enabled); diff --git a/src/plugins/presentation_util/public/services/mocks.ts b/src/plugins/presentation_util/public/services/mocks.ts new file mode 100644 index 00000000000000..d0f54c5af5f094 --- /dev/null +++ b/src/plugins/presentation_util/public/services/mocks.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; +import { CoreStart } from '@kbn/core/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; + +import { setKibanaServices } from './kibana_services'; + +const setDefaultPresentationUtilCapabilities = (core: CoreStart) => { + core.application.capabilities = { + ...core.application.capabilities, + dashboard: { + show: true, + createNew: true, + }, + visualize: { + save: true, + }, + advancedSettings: { + save: true, + }, + }; +}; + +export const setStubKibanaServices = () => { + const core = coreMock.createStart(); + + setDefaultPresentationUtilCapabilities(core); + setKibanaServices(core, { + contentManagement: contentManagementMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), + }); +}; diff --git a/src/plugins/presentation_util/public/services/plugin_services.story.ts b/src/plugins/presentation_util/public/services/plugin_services.story.ts deleted file mode 100644 index 76118233a4f149..00000000000000 --- a/src/plugins/presentation_util/public/services/plugin_services.story.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - PluginServices, - PluginServiceProviders, - PluginServiceProvider, - PluginServiceRegistry, -} from './create'; -import { PresentationUtilServices } from './types'; - -import { capabilitiesServiceFactory } from './capabilities/capabilities.story'; -import { dataViewsServiceFactory } from './data_views/data_views.story'; -import { contentManagementServiceFactory } from './content_management/content_management.stub'; -import { labsServiceFactory } from './labs/labs.story'; -import { uiActionsServiceFactory } from './ui_actions/ui_actions.stub'; - -export const providers: PluginServiceProviders = { - capabilities: new PluginServiceProvider(capabilitiesServiceFactory), - labs: new PluginServiceProvider(labsServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - contentManagement: new PluginServiceProvider(contentManagementServiceFactory), - uiActions: new PluginServiceProvider(uiActionsServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry(providers); - -export interface StorybookParams { - canAccessDashboards?: boolean; - canCreateNewDashboards?: boolean; - canSaveVisualizations?: boolean; - canSetAdvancedSettings?: boolean; -} diff --git a/src/plugins/presentation_util/public/services/plugin_services.stub.ts b/src/plugins/presentation_util/public/services/plugin_services.stub.ts deleted file mode 100644 index c503af55bd8763..00000000000000 --- a/src/plugins/presentation_util/public/services/plugin_services.stub.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServices, PluginServiceProviders, PluginServiceProvider } from './create'; -import { PresentationUtilServices } from './types'; -import { registry as stubRegistry } from './plugin_services.story'; -import { PresentationUtilPluginStart, registerExpressionsLanguage } from '..'; - -import { capabilitiesServiceFactory } from './capabilities/capabilities.story'; -import { dataViewsServiceFactory } from './data_views/data_views.story'; -import { labsServiceFactory } from './labs/labs.story'; -import { uiActionsServiceFactory } from './ui_actions/ui_actions.stub'; -import { contentManagementServiceFactory } from './content_management/content_management.stub'; - -export const providers: PluginServiceProviders = { - contentManagement: new PluginServiceProvider(contentManagementServiceFactory), - capabilities: new PluginServiceProvider(capabilitiesServiceFactory), - labs: new PluginServiceProvider(labsServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - uiActions: new PluginServiceProvider(uiActionsServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const getStubPluginServices = (): PresentationUtilPluginStart => { - pluginServices.setRegistry(stubRegistry.start({})); - return { - ContextProvider: pluginServices.getContextProvider(), - labsService: pluginServices.getServices().labs, - registerExpressionsLanguage, - }; -}; diff --git a/src/plugins/presentation_util/public/services/plugin_services.ts b/src/plugins/presentation_util/public/services/plugin_services.ts deleted file mode 100644 index 4d43a0e4c4a744..00000000000000 --- a/src/plugins/presentation_util/public/services/plugin_services.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - PluginServices, - PluginServiceProviders, - KibanaPluginServiceParams, - PluginServiceProvider, - PluginServiceRegistry, -} from './create'; -import { PresentationUtilPluginStartDeps } from '../types'; - -import { capabilitiesServiceFactory } from './capabilities/capabilities_service'; -import { dataViewsServiceFactory } from './data_views/data_views_service'; -import { contentManagementServiceFactory } from './content_management/content_management_service'; -import { uiActionsServiceFactory } from './ui_actions/ui_actions_service'; -import { labsServiceFactory } from './labs/labs_service'; -import { PresentationUtilServices } from './types'; - -export const providers: PluginServiceProviders< - PresentationUtilServices, - KibanaPluginServiceParams -> = { - capabilities: new PluginServiceProvider(capabilitiesServiceFactory), - labs: new PluginServiceProvider(labsServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - uiActions: new PluginServiceProvider(uiActionsServiceFactory), - contentManagement: new PluginServiceProvider(contentManagementServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry< - PresentationUtilServices, - KibanaPluginServiceParams ->(providers); diff --git a/src/plugins/presentation_util/public/services/labs/labs_service.ts b/src/plugins/presentation_util/public/services/presentation_labs_service.ts similarity index 58% rename from src/plugins/presentation_util/public/services/labs/labs_service.ts rename to src/plugins/presentation_util/public/services/presentation_labs_service.ts index b30a7ee89bdde6..b69f03d7c18a13 100644 --- a/src/plugins/presentation_util/public/services/labs/labs_service.ts +++ b/src/plugins/presentation_util/public/services/presentation_labs_service.ts @@ -7,43 +7,31 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import { EnvironmentName, - projectIDs, - projects, - ProjectID, + EnvironmentStatus, + LABS_PROJECT_PREFIX, Project, - getProjectIDs, + ProjectConfig, + ProjectID, SolutionName, - LABS_PROJECT_PREFIX, -} from '../../../common'; -import { PresentationUtilPluginStartDeps } from '../../types'; -import { KibanaPluginServiceFactory } from '../create'; -import { - PresentationLabsService, - isEnabledByStorageValue, - setStorageStatus, - setUISettingsStatus, - applyProjectStatus, -} from './types'; - -export type LabsServiceFactory = KibanaPluginServiceFactory< - PresentationLabsService, - PresentationUtilPluginStartDeps ->; + isProjectEnabledByStatus, + projectIDs, + projects, +} from '../../common'; +import { coreServices } from './kibana_services'; -const clearLabsFromStorage = (storage: Storage) => { - projectIDs.forEach((projectID) => storage.removeItem(projectID)); +export interface PresentationLabsService { + isProjectEnabled: (id: ProjectID) => boolean; + getProject: (id: ProjectID) => Project; + getProjects: (solutions?: SolutionName[]) => Record; + setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; + reset: () => void; +} - // This is a redundancy, to catch any labs that may have been removed above. - // We could consider gathering telemetry to see how often this happens, or this may be unnecessary. - Object.keys(storage) - .filter((key) => key.startsWith(LABS_PROJECT_PREFIX)) - .forEach((key) => storage.removeItem(key)); -}; - -export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { - const { uiSettings } = coreStart; +export const getPresentationLabsService = (): PresentationLabsService => { + const { uiSettings } = coreServices; const localStorage = window.localStorage; const sessionStorage = window.sessionStorage; @@ -94,7 +82,6 @@ export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; return { - getProjectIDs, getProjects, getProject, isProjectEnabled, @@ -102,3 +89,71 @@ export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { setProjectStatus, }; }; + +/** + * Helpers + */ +const isEnabledByStorageValue = ( + project: ProjectConfig, + environment: EnvironmentName, + value: string | boolean | null +): boolean => { + const defaultValue = project.isActive; + + if (!project.environments.includes(environment)) { + return defaultValue; + } + + if (value === true || value === false) { + return value; + } + + if (value === 'enabled') { + return true; + } + + if (value === 'disabled') { + return false; + } + + return defaultValue; +}; + +const setStorageStatus = (storage: Storage, id: ProjectID, enabled: boolean) => + storage.setItem(id, enabled ? 'enabled' : 'disabled'); + +export const applyProjectStatus = (project: ProjectConfig, status: EnvironmentStatus): Project => { + const { isActive, environments } = project; + + environments.forEach((name) => { + if (!environments.includes(name)) { + delete status[name]; + } + }); + + const isEnabled = isProjectEnabledByStatus(isActive, status); + const isOverride = isEnabled !== isActive; + + return { + ...project, + status: { + ...status, + defaultValue: isActive, + isEnabled, + isOverride, + }, + }; +}; + +const setUISettingsStatus = (client: IUiSettingsClient, id: ProjectID, enabled: boolean) => + client.set(id, enabled); + +const clearLabsFromStorage = (storage: Storage) => { + projectIDs.forEach((projectID) => storage.removeItem(projectID)); + + // This is a redundancy, to catch any labs that may have been removed above. + // We could consider gathering telemetry to see how often this happens, or this may be unnecessary. + Object.keys(storage) + .filter((key) => key.startsWith(LABS_PROJECT_PREFIX)) + .forEach((key) => storage.removeItem(key)); +}; diff --git a/src/plugins/presentation_util/public/services/types.ts b/src/plugins/presentation_util/public/services/types.ts deleted file mode 100644 index 06247bb22739b5..00000000000000 --- a/src/plugins/presentation_util/public/services/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PresentationLabsService } from './labs/types'; -import { PresentationCapabilitiesService } from './capabilities/types'; -import { PresentationDataViewsService } from './data_views/types'; -import { PresentationUiActionsService } from './ui_actions/types'; -import { PresentationContentManagementService } from './content_management/types'; - -export interface PresentationUtilServices { - contentManagement: PresentationContentManagementService; - capabilities: PresentationCapabilitiesService; - dataViews: PresentationDataViewsService; - uiActions: PresentationUiActionsService; - labs: PresentationLabsService; -} - -export type { PresentationCapabilitiesService, PresentationLabsService }; diff --git a/src/plugins/presentation_util/public/services/ui_actions/types.ts b/src/plugins/presentation_util/public/services/ui_actions/types.ts deleted file mode 100644 index 4953fb6b372347..00000000000000 --- a/src/plugins/presentation_util/public/services/ui_actions/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; - -export interface PresentationUiActionsService { - getTriggerCompatibleActions: UiActionsStart['getTriggerCompatibleActions']; -} diff --git a/src/plugins/presentation_util/public/services/ui_actions/ui_actions.stub.ts b/src/plugins/presentation_util/public/services/ui_actions/ui_actions.stub.ts deleted file mode 100644 index 05399418b60150..00000000000000 --- a/src/plugins/presentation_util/public/services/ui_actions/ui_actions.stub.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; - -import { PluginServiceFactory } from '../create'; -import { PresentationUiActionsService } from './types'; - -type CapabilitiesServiceFactory = PluginServiceFactory; - -export const uiActionsServiceFactory: CapabilitiesServiceFactory = () => { - const { getTriggerCompatibleActions } = uiActionsPluginMock.createStartContract(); - return { getTriggerCompatibleActions }; -}; diff --git a/src/plugins/presentation_util/public/services/ui_actions/ui_actions_service.ts b/src/plugins/presentation_util/public/services/ui_actions/ui_actions_service.ts deleted file mode 100644 index cdce13e10ae940..00000000000000 --- a/src/plugins/presentation_util/public/services/ui_actions/ui_actions_service.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PresentationUtilPluginStartDeps } from '../../types'; -import { PresentationUiActionsService } from './types'; -import { KibanaPluginServiceFactory } from '../create'; - -export type UiActionsServiceFactory = KibanaPluginServiceFactory< - PresentationUiActionsService, - PresentationUtilPluginStartDeps ->; - -export const uiActionsServiceFactory: UiActionsServiceFactory = ({ startPlugins }) => { - const { - uiActions: { getTriggerCompatibleActions }, - } = startPlugins; - return { - getTriggerCompatibleActions, - }; -}; diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts index 2f592796d2524f..c0f4005f000fec 100644 --- a/src/plugins/presentation_util/public/types.ts +++ b/src/plugins/presentation_util/public/types.ts @@ -7,18 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { FC, PropsWithChildren } from 'react'; -import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { registerExpressionsLanguage } from '.'; -import { PresentationLabsService } from './services/labs/types'; +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { type PresentationLabsService, registerExpressionsLanguage } from '.'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PresentationUtilPluginSetup {} export interface PresentationUtilPluginStart { - ContextProvider: FC>; labsService: PresentationLabsService; registerExpressionsLanguage: typeof registerExpressionsLanguage; } diff --git a/src/plugins/presentation_util/public/utils/get_presentation_capabilities.ts b/src/plugins/presentation_util/public/utils/get_presentation_capabilities.ts new file mode 100644 index 00000000000000..bdc547c2f52023 --- /dev/null +++ b/src/plugins/presentation_util/public/utils/get_presentation_capabilities.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { coreServices } from '../services/kibana_services'; + +interface PresentationCapabilities { + canAccessDashboards: boolean; + canCreateNewDashboards: boolean; + canSaveVisualizations: boolean; + canSetAdvancedSettings: boolean; +} + +export const getPresentationCapabilities = (): PresentationCapabilities => { + const { dashboard, visualize, advancedSettings } = coreServices.application.capabilities; + + return { + canAccessDashboards: Boolean(dashboard.show), + canCreateNewDashboards: Boolean(dashboard.createNew), + canSaveVisualizations: Boolean(visualize.save), + canSetAdvancedSettings: Boolean(advancedSettings.save), + }; +}; diff --git a/src/plugins/presentation_util/storybook/decorator.tsx b/src/plugins/presentation_util/storybook/decorator.tsx index 4e1eaa9ec47421..214ab42176ce11 100644 --- a/src/plugins/presentation_util/storybook/decorator.tsx +++ b/src/plugins/presentation_util/storybook/decorator.tsx @@ -9,13 +9,10 @@ import React from 'react'; -import { DecoratorFn } from '@storybook/react'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaContextProvider as KibanaReactProvider } from '@kbn/kibana-react-plugin/public'; -import { pluginServices } from '../public/services'; -import { PresentationUtilServices } from '../public/services/types'; -import { providers, StorybookParams } from '../public/services/plugin_services.story'; -import { PluginServiceRegistry } from '../public/services/create'; +import { DecoratorFn } from '@storybook/react'; +import { setStubKibanaServices } from '../public/services/mocks'; const settings = new Map(); settings.set('darkMode', true); @@ -33,15 +30,11 @@ const services = { }; export const servicesContextDecorator: DecoratorFn = (story: Function, storybook) => { - const registry = new PluginServiceRegistry(providers); - pluginServices.setRegistry(registry.start(storybook.args)); - const ContextProvider = pluginServices.getContextProvider(); + setStubKibanaServices(); return ( - - {story()} - + {story()} ); }; diff --git a/src/plugins/presentation_util/storybook/main.ts b/src/plugins/presentation_util/storybook/main.ts index e3b5b1c6c596a0..c51873a09b7f23 100644 --- a/src/plugins/presentation_util/storybook/main.ts +++ b/src/plugins/presentation_util/storybook/main.ts @@ -9,4 +9,10 @@ import { defaultConfig } from '@kbn/storybook'; -module.exports = defaultConfig; +module.exports = { + ...defaultConfig, + define: { + global: 'window', + }, + stories: ['../../**/*.stories.+(tsx|mdx)'], +}; diff --git a/src/plugins/presentation_util/storybook/preview.tsx b/src/plugins/presentation_util/storybook/preview.tsx index 4ddf9e5080d293..a6b872f2d52cfe 100644 --- a/src/plugins/presentation_util/storybook/preview.tsx +++ b/src/plugins/presentation_util/storybook/preview.tsx @@ -8,13 +8,18 @@ */ import React from 'react'; +import * as jest from 'jest-mock'; import { addDecorator } from '@storybook/react'; + import { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks'; import { servicesContextDecorator } from './decorator'; addDecorator(servicesContextDecorator); +// @ts-ignore +window.jest = jest; + export const parameters = { docs: { page: () => ( diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index dea02739d9db97..4e18f2e8bce2b1 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -36,6 +36,7 @@ "@kbn/calculate-width-from-char-count", "@kbn/field-utils", "@kbn/presentation-publishing", + "@kbn/core-ui-settings-browser", ], "exclude": ["target/**/*"] } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 626bab7b9e0ed5..2ba40ef14d38a2 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -79,7 +79,7 @@ const createInstance = async () => { savedObjectsTaggingOss: savedObjectTaggingOssPluginMock.createStart(), savedSearch: savedSearchPluginMock.createStartContract(), navigation: navigationPluginMock.createStartContract(), - presentationUtil: presentationUtilPluginMock.createStartContract(coreMock.createStart()), + presentationUtil: presentationUtilPluginMock.createStartContract(), urlForwarding: urlForwardingPluginMock.createStartContract(), screenshotMode: screenshotModePluginMock.createStartContract(), fieldFormats: fieldFormatsServiceMock.createStartContract(), diff --git a/src/plugins/visualizations/public/visualize_app/index.tsx b/src/plugins/visualizations/public/visualize_app/index.tsx index 29509468ea44ac..375df3a4677909 100644 --- a/src/plugins/visualizations/public/visualize_app/index.tsx +++ b/src/plugins/visualizations/public/visualize_app/index.tsx @@ -35,17 +35,15 @@ export const renderApp = ( - - - - - + + + diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx index 0d49b38319130c..5c5a85fbf06efd 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx +++ b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx @@ -623,7 +623,7 @@ export const getTopNavConfig = ( ); } - showSaveModal(saveModal, presentationUtil.ContextProvider); + showSaveModal(saveModal); }, }, ] diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx index 36f8f3e2ddaf69..420e2b510c62ee 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx @@ -82,39 +82,34 @@ export const ChangePointDetectionAppState: FC appContextValue.embeddingOrigin = AIOPS_EMBEDDABLE_ORIGIN.ML_AIOPS_LABS; - const PresentationContextProvider = - appContextValue.presentationUtil?.ContextProvider ?? React.Fragment; - const CasesContext = appContextValue.cases?.ui.getCasesContext() ?? React.Fragment; const casesPermissions = appContextValue.cases?.helpers.canUseCases(); return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts index 2c926797ac07f0..de563f8f5a38da 100644 --- a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts +++ b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts @@ -24,7 +24,6 @@ import type { ThemeServiceStart, } from '@kbn/core/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; -import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; @@ -105,7 +104,6 @@ export interface AiopsAppContextValue { * Internationalisation service */ i18n: CoreStart['i18n']; - presentationUtil?: PresentationUtilPluginStart; embeddable?: EmbeddableStart; cases?: CasesPublicStart; isServerless?: boolean; diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index d9a279e2d26196..72440e3e6873c3 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -64,7 +64,6 @@ export const renderApp = ({ canvasStore: Store; pluginServices: PluginServices; }) => { - const { presentationUtil } = startPlugins; const { element } = params; element.classList.add('canvas'); element.classList.add('canvasContainerWrapper'); @@ -75,11 +74,9 @@ export const renderApp = ({ - - - - - + + + diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx index 4c610a943a1f2c..b5bb0710c3de29 100644 --- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -28,14 +28,7 @@ export const servicesContextDecorator = (): DecoratorFn => { storybook.args.useStaticData = true; } - pluginServices.setRegistry(pluginServiceRegistry.start(storybook.args)); - const ContextProvider = pluginServices.getContextProvider(); - - return ( - - {story()} - - ); + return {story()}; }; }; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 07e1627f6d0669..7f91943eade30a 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, PropsWithChildren, useCallback, useEffect, useState, useMemo } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { AppMountParameters, CoreSetup, CoreStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { RouteComponentProps } from 'react-router-dom'; @@ -147,18 +147,11 @@ export async function mountApp( mountProps: { createEditorFrame: EditorFrameStart['createInstance']; attributeService: LensAttributeService; - getPresentationUtilContext: () => FC>; topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[]; locator?: LensAppLocator; } ) { - const { - createEditorFrame, - attributeService, - getPresentationUtilContext, - topNavMenuEntryGenerators, - locator, - } = mountProps; + const { createEditorFrame, attributeService, topNavMenuEntryGenerators, locator } = mountProps; const [[coreStart, startDependencies], instance] = await Promise.all([ core.getStartServices(), createEditorFrame(), @@ -411,25 +404,21 @@ export async function mountApp( params.element.classList.add('lnsAppWrapper'); - const PresentationUtilContext = getPresentationUtilContext(); - render( - - - - - } - /> - - - - - + + + + } + /> + + + + , params.element diff --git a/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx index 24eeffd283f5f2..05aaa5a59c75ca 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx @@ -57,13 +57,9 @@ export function getSaveModalComponent( return ; } - const { ContextProvider: PresentationUtilContext } = lensServices.presentationUtil; - return ( - - - + ); }; diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index fa98db785c50e8..18fa29fd6caf29 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -145,7 +145,7 @@ export function makeDefaultServices( inspect: jest.fn(), close: jest.fn(), }, - presentationUtil: presentationUtilPluginMock.createStartContract(core), + presentationUtil: presentationUtilPluginMock.createStartContract(), savedObjectStore: { load: jest.fn(), search: jest.fn(), diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 80dfa0f0398bce..b2293ea43b1099 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -455,9 +455,6 @@ export class LensPlugin { () => startServices().plugins.data.nowProvider.get() ); - const getPresentationUtilContext = () => - startServices().plugins.presentationUtil.ContextProvider; - core.application.register({ id: APP_ID, title: NOT_INTERNATIONALIZED_PRODUCT_NAME, @@ -490,7 +487,6 @@ export class LensPlugin { return mountApp(core, params, { createEditorFrame: frameStart.createInstance, attributeService: getLensAttributeService(coreStart, deps), - getPresentationUtilContext, topNavMenuEntryGenerators: this.topNavMenuEntries, locator: this.locator, }); diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index e6f65fc67866d1..ea31df8734ade0 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -89,7 +89,6 @@ export const getNavigateToApp = () => coreStart.application.navigateToApp; export const getUrlForApp = () => coreStart.application.getUrlForApp; export const getNavigateToUrl = () => coreStart.application.navigateToUrl; export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; -export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider; export const getSpacesApi = () => pluginsStart.spaces; export const getTheme = () => coreStart.theme; export const getApplication = () => coreStart.application; diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 706b3b58ca979a..d9b7aec6612f3a 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -24,7 +24,6 @@ import { getInspector, getCoreOverlays, getSavedObjectsTagging, - getPresentationUtilContext, } from '../../kibana_services'; import { MAP_EMBEDDABLE_NAME } from '../../../common/constants'; import { SavedMap } from './saved_map'; @@ -210,7 +209,6 @@ export function getTopNavConfig({ defaultMessage: 'map', }), }; - const PresentationUtilContext = getPresentationUtilContext(); let saveModal; @@ -251,7 +249,7 @@ export function getTopNavConfig({ ); } - showSaveModal(saveModal, PresentationUtilContext); + showSaveModal(saveModal); }, }); diff --git a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx index 146a3efc9cfcd6..f9c2e7846be91f 100644 --- a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx +++ b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx @@ -64,7 +64,6 @@ export const ChangePointDetectionPage: FC = () => { 'i18n', 'lens', 'notifications', - 'presentationUtil', 'share', 'storage', 'theme', diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx index d61849436c73d9..87983d2c61603f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer/state_manager.tsx @@ -39,7 +39,7 @@ export const ExplorerUrlStateManager: FC = ({ jobsWithTimeRange, }) => { const { - services: { cases, presentationUtil, uiSettings, mlServices }, + services: { cases, uiSettings, mlServices }, } = useMlKibana(); const { mlApi } = mlServices; @@ -194,7 +194,6 @@ export const ExplorerUrlStateManager: FC = ({ } const CasesContext = cases?.ui.getCasesContext() ?? React.Fragment; - const PresentationContextProvider = presentationUtil?.ContextProvider ?? React.Fragment; const casesPermissions = cases?.helpers.canUseCases(); @@ -218,27 +217,25 @@ export const ExplorerUrlStateManager: FC = ({ - - {jobsWithTimeRange.length === 0 ? ( - - ) : ( - - )} - + {jobsWithTimeRange.length === 0 ? ( + + ) : ( + + )}
); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx index 982884911e5821..d673aca1e74e3d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx @@ -33,9 +33,8 @@ export const TimeSeriesExplorerPage: FC { const { - services: { cases, docLinks, presentationUtil }, + services: { cases, docLinks }, } = useMlKibana(); - const PresentationContextProvider = presentationUtil?.ContextProvider ?? React.Fragment; const CasesContext = cases?.ui.getCasesContext() ?? React.Fragment; const casesPermissions = cases?.helpers.canUseCases(); const helpLink = docLinks.links.ml.anomalyDetection; @@ -65,7 +64,7 @@ export const TimeSeriesExplorerPage: FC )} - {children} + {children} diff --git a/x-pack/plugins/observability_solution/observability/public/application/index.tsx b/x-pack/plugins/observability_solution/observability/public/application/index.tsx index 9c56ec1bb54ecd..3ae624d2f8ea10 100644 --- a/x-pack/plugins/observability_solution/observability/public/application/index.tsx +++ b/x-pack/plugins/observability_solution/observability/public/application/index.tsx @@ -83,52 +83,46 @@ export const renderApp = ({ const ApplicationUsageTrackingProvider = usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; const CloudProvider = plugins.cloud?.CloudContextProvider ?? React.Fragment; - const PresentationContextProvider = plugins.presentationUtil?.ContextProvider ?? React.Fragment; ReactDOM.render( - - - - - + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + , element ); diff --git a/x-pack/plugins/observability_solution/slo/public/application.tsx b/x-pack/plugins/observability_solution/slo/public/application.tsx index de8851d2dd67c5..b6291b5286148e 100644 --- a/x-pack/plugins/observability_solution/slo/public/application.tsx +++ b/x-pack/plugins/observability_solution/slo/public/application.tsx @@ -82,8 +82,6 @@ export const renderApp = ({ usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; const CloudProvider = plugins.cloud?.CloudContextProvider ?? React.Fragment; - const PresentationContextProvider = plugins.presentationUtil?.ContextProvider ?? React.Fragment; - const unregisterPrompts = plugins.observabilityAIAssistant?.service.setScreenContext({ starterPrompts: [ { @@ -109,50 +107,45 @@ export const renderApp = ({ ReactDOM.render( - - - - - + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + , element ); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx index 014043efa5cee8..0dd010e7b78e3d 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -24,15 +24,8 @@ import { PageRouter } from './routes'; import { setBasePath, store } from './state'; const Application = (props: SyntheticsAppProps) => { - const { - basePath, - canSave, - coreStart, - startPlugins, - renderGlobalHelpControls, - setBadge, - appMountParameters, - } = props; + const { basePath, canSave, coreStart, renderGlobalHelpControls, setBadge, appMountParameters } = + props; useEffect(() => { renderGlobalHelpControls(); @@ -55,9 +48,6 @@ const Application = (props: SyntheticsAppProps) => { store.dispatch(setBasePath(basePath)); - const PresentationContextProvider = - startPlugins.presentationUtil?.ContextProvider ?? React.Fragment; - return ( { }} > - - - - -
- - - - - -
-
-
-
-
+ + + +
+ + + + + +
+
+
+
From 8af920bc8bc57c059da0fc97eab12d61daef9313 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:38:48 +0200 Subject: [PATCH 15/50] [DOCS] Fix list format (#195349) --- docs/user/ml/index.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index c13c1127124e62..e84ca23dbc84d6 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -22,12 +22,14 @@ image::user/ml/images/ml-data-visualizer-sample.png[{data-viz} for sample flight You can upload different file formats for analysis with the *{data-viz}*. File formats supported up to 500 MB: + * CSV * TSV * NDJSON * Log files File formats supported up to 60 MB: + * PDF * Microsoft Office files (Word, Excel, PowerPoint) * Plain Text (TXT) @@ -258,4 +260,4 @@ menu. If the split field is selected, you can either select specific charts (partitions) or set the maximum number of top change points to plot. It's possible to preserve the applied time range or use the time bound from the page date picker. You can also add or edit change point charts directly from the -**Dashboard** app. \ No newline at end of file +**Dashboard** app. From d21495bbce07ed39fdcf077c9170d012adab74a4 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 8 Oct 2024 17:52:33 +0200 Subject: [PATCH 16/50] [Synthetics] Format locations for public API's (#195295) ## Summary Fixes https://github.com/elastic/kibana/issues/194901 !! Format locations for public API's !! Public API has been refactored to return list of location ids just like they are provided in request !! --- .../runtime_types/monitor_management/state.ts | 1 + .../apps/synthetics/state/monitor_list/api.ts | 1 + .../synthetics/server/routes/common.ts | 5 ++ .../saved_object_to_monitor.test.ts | 53 ++++++++++++++----- .../formatters/saved_object_to_monitor.ts | 25 ++++++++- .../routes/monitor_cruds/get_monitors_list.ts | 1 + .../synthetics/synthetics_rule_helper.ts | 2 +- .../apis/synthetics/add_monitor_project.ts | 5 +- 8 files changed, 76 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts index 0c546eb431deae..6bde68b526723f 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts @@ -20,6 +20,7 @@ export const FetchMonitorManagementListQueryArgsCodec = t.partial({ projects: t.array(t.string), schedules: t.array(t.string), monitorQueryIds: t.array(t.string), + internal: t.boolean, }); export type FetchMonitorManagementListQueryArgs = t.TypeOf< diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts index bab1062a683a4d..245c2be23f9c13 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts @@ -35,6 +35,7 @@ function toMonitorManagementListQueryArgs( schedules: pageState.schedules, monitorQueryIds: pageState.monitorQueryIds, searchFields: [], + internal: true, }; } diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts index 2edb17a77a635c..c4444afcc06e8e 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts @@ -35,6 +35,11 @@ export const QuerySchema = schema.object({ status: StringOrArraySchema, searchAfter: schema.maybe(schema.arrayOf(schema.string())), monitorQueryIds: StringOrArraySchema, + internal: schema.maybe( + schema.boolean({ + defaultValue: false, + }) + ), }); export type MonitorsQuery = TypeOf; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.test.ts index 0ddfda0f778749..40aec3b468a37c 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.test.ts @@ -84,17 +84,8 @@ describe('mergeSourceMonitor', () => { id: 'todos-lightweight-test-projects-default', ipv4: true, ipv6: true, - locations: [ - { - geo: { - lat: 41.25, - lon: -95.86, - }, - id: 'us_central', - isServiceManaged: true, - label: 'North America - US Central', - }, - ], + locations: ['us_central', 'us_east'], + private_locations: ['pvt_us_east'], max_redirects: 0, mode: 'any', name: 'Todos Lightweight', @@ -166,13 +157,31 @@ describe('mergeSourceMonitor', () => { locations: [ { geo: { - lat: 41.25, lon: -95.86, + lat: 41.25, }, - id: 'us_central', isServiceManaged: true, + id: 'us_central', label: 'North America - US Central', }, + { + geo: { + lon: -95.86, + lat: 41.25, + }, + isServiceManaged: true, + id: 'us-east4-a', + label: 'US East', + }, + { + geo: { + lon: -95.86, + lat: 41.25, + }, + isServiceManaged: false, + id: 'pvt_us_east', + label: 'US East (Private)', + }, ], max_redirects: '0', mode: 'any', @@ -240,6 +249,24 @@ const testMonitor = { id: 'us_central', label: 'North America - US Central', }, + { + geo: { + lon: -95.86, + lat: 41.25, + }, + isServiceManaged: true, + id: 'us-east4-a', + label: 'US East', + }, + { + geo: { + lon: -95.86, + lat: 41.25, + }, + isServiceManaged: false, + id: 'pvt_us_east', + label: 'US East (Private)', + }, ], namespace: 'default', origin: 'project', diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.ts index 4f5a3b194c8962..06746fc235769d 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.ts @@ -7,6 +7,7 @@ import { SavedObject } from '@kbn/core/server'; import { mergeWith, omit, omitBy } from 'lodash'; +import { LocationsMap } from '../../../synthetics_service/project_monitor/normalizers/common_fields'; import { ConfigKey, EncryptedSyntheticsMonitor, @@ -38,11 +39,14 @@ type Result = MonitorFieldsResult & { ssl: Record; response: Record; check: Record; + locations: string[]; + private_locations: string[]; }; export const transformPublicKeys = (result: Result) => { let formattedResult = { ...result, + ...formatLocations(result), [ConfigKey.PARAMS]: formatParams(result), retest_on_failure: (result[ConfigKey.MAX_ATTEMPTS] ?? 1) > 1, ...(result[ConfigKey.HOSTS] && { host: result[ConfigKey.HOSTS] }), @@ -66,8 +70,7 @@ export const transformPublicKeys = (result: Result) => { return omitBy( res, - (value, key) => - key.startsWith('response.') || key.startsWith('ssl.') || key.startsWith('check.') + (_, key) => key.startsWith('response.') || key.startsWith('ssl.') || key.startsWith('check.') ); }; @@ -105,6 +108,24 @@ const customizer = (destVal: any, srcValue: any, key: string) => { } }; +const formatLocations = (config: MonitorFields) => { + const locMap = Object.entries(LocationsMap); + const locations = config[ConfigKey.LOCATIONS] + ?.filter((location) => location.isServiceManaged) + .map((location) => { + return locMap.find(([_key, value]) => value === location.id)?.[0] ?? location.id; + }); + + const privateLocations = config[ConfigKey.LOCATIONS] + ?.filter((location) => !location.isServiceManaged) + .map((location) => location.id); + + return { + ...(locations && { locations }), + ...(privateLocations && { private_locations: privateLocations }), + }; +}; + const formatParams = (config: MonitorFields) => { if (config[ConfigKey.PARAMS]) { try { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts index 62f60f6b5636c2..ed99e2b1d64d00 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts @@ -45,6 +45,7 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => monitors: savedObjects.map((monitor) => mapSavedObjectToMonitor({ monitor, + internal: request.query?.internal, }) ), absoluteTotal, diff --git a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts index 9321bc0935a802..2915cb5ee5d3b7 100644 --- a/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts +++ b/x-pack/test/alerting_api_integration/observability/synthetics/synthetics_rule_helper.ts @@ -143,7 +143,7 @@ export class SyntheticsRuleHelper { schedule: 1, }; const res = await this.supertest - .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) + .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '?internal=true') .set('kbn-xsrf', 'true') .send(testData); diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index 9654a2bc434040..ea8821901c9e9d 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -1814,7 +1814,10 @@ export default function ({ getService }: FtrProviderContext) { projectMonitors.monitors.map((monitor) => { return supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${monitor.id}` }) + .query({ + filter: `${syntheticsMonitorType}.attributes.journey_id: ${monitor.id}`, + internal: true, + }) .set('kbn-xsrf', 'true') .expect(200); }) From 11ff4a9b2e1c4e7fb0375caf2ee9d1ab2ced86dd Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 8 Oct 2024 12:58:15 -0300 Subject: [PATCH 17/50] Fix monaco editor functional test service check (#195154) ## Summary This PR fixes a monaco editor functional test service check that was added in #193851. The check validates that the editor updates with the correct text content, but it looks like it may run a bit too quickly sometimes before the DOM has a chance to update. Fixes #194511. Fixes #194482. Fixes #194425. Fixes #194305. Flaky test runs: - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7099 ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- test/functional/services/monaco_editor.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/functional/services/monaco_editor.ts b/test/functional/services/monaco_editor.ts index 43ce38aa2c7873..5e942c8d7e7ad6 100644 --- a/test/functional/services/monaco_editor.ts +++ b/test/functional/services/monaco_editor.ts @@ -63,11 +63,13 @@ export class MonacoEditorService extends FtrService { value ); }); - const newCodeEditorValue = await this.getCodeEditorValue(nthIndex); - expect(newCodeEditorValue).equal( - value, - `Expected value was: ${value}, but got: ${newCodeEditorValue}` - ); + await this.retry.try(async () => { + const newCodeEditorValue = await this.getCodeEditorValue(nthIndex); + expect(newCodeEditorValue).equal( + value, + `Expected value was: ${value}, but got: ${newCodeEditorValue}` + ); + }); } public async getCurrentMarkers(testSubjId: string) { From e1cf8d5e825a984779013b2cc3a4baf42c726a0c Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Tue, 8 Oct 2024 18:12:09 +0200 Subject: [PATCH 18/50] Change ca certificate path config key. (#195233) towards: https://github.com/elastic/response-ops-team/issues/209 This PR changes ca path key in the config . --- x-pack/plugins/actions/server/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 399f92090818b1..d806bde1fa2276 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -147,7 +147,7 @@ export const configSchema = schema.object({ ), usage: schema.maybe( schema.object({ - cert: schema.maybe( + ca: schema.maybe( schema.object({ path: schema.string(), }) From d03537f21764fda1fbafcfec03391b3e03b64e82 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 8 Oct 2024 18:17:17 +0200 Subject: [PATCH 19/50] [React@18] fix incorrect `useEffect` return value (#195421) --- .../components/elasticsearch_guide/elasticsearch_guide.tsx | 4 +++- .../public/applications/shared/api_key/api_key_panel.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx index f89547b00c3cf2..80a8de9acdc21f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx @@ -35,7 +35,9 @@ export const ElasticsearchGuide = () => { const { data } = useValues(FetchApiKeysAPILogic); const apiKeys = data?.api_keys || []; - useEffect(() => makeRequest({}), []); + useEffect(() => { + makeRequest({}); + }, []); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx index c5d1034f488d81..34c7ac66343c9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx @@ -42,7 +42,9 @@ export const ApiKeyPanel: React.FC = () => { const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const elasticsearchEndpoint = esConfig.elasticsearch_host; - useEffect(() => makeRequest({}), []); + useEffect(() => { + makeRequest({}); + }, []); const apiKeys = data?.api_keys || []; const cloudId = cloud?.cloudId; From de9fa3d6db9bbfa9b030676037d4c10c4bcddf35 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 03:19:18 +1100 Subject: [PATCH 20/50] skip failing test suite (#178404) --- .../cypress/e2e/all/alerts_automated_action_results.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts index 4c7c9663b2d40e..31bcd68da19cd8 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts @@ -11,7 +11,8 @@ import { checkActionItemsInResults, loadRuleAlerts } from '../../tasks/live_quer const UUID_REGEX = '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}'; -describe('Alert Flyout Automated Action Results', () => { +// Failing: See https://github.com/elastic/kibana/issues/178404 +describe.skip('Alert Flyout Automated Action Results', () => { let ruleId: string; before(() => { From f86d25f99acd6ff493f28dceb3f10e38cd4f0ed3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 03:20:18 +1100 Subject: [PATCH 21/50] skip failing test suite (#195453) --- x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts index 56c0b478d96215..b6c210ece2ddc9 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts @@ -43,7 +43,8 @@ import { import { ServerlessRoleName } from '../../support/roles'; import { getAdvancedButton } from '../../screens/integrations'; -describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/195453 +describe.skip('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { let caseId: string; before(() => { From 8e2a55214de6c167dc9e918d8ab44652b22529d3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 03:21:00 +1100 Subject: [PATCH 22/50] skip failing test suite (#172418) --- .../e2e/response_actions/response_console/release.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts index d11b7210713a82..96702227b70b97 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/release.cy.ts @@ -27,7 +27,8 @@ import { enableAllPolicyProtections } from '../../../tasks/endpoint_policy'; import { createEndpointHost } from '../../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_data'; -describe('Response console', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/172418 +describe.skip('Response console', { tags: ['@ess', '@serverless'] }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; From 80a5565aeee19589e1913d3f82ed875aabc573f2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 03:21:23 +1100 Subject: [PATCH 23/50] skip failing test suite (#195463) --- x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts index dbed4e56f88a0b..39a54d29d40257 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts @@ -38,7 +38,8 @@ import { loadSavedQuery, cleanupSavedQuery, cleanupPack, loadPack } from '../../ import { request } from '../../tasks/common'; import { ServerlessRoleName } from '../../support/roles'; -describe('Packs - Create and Edit', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/195463 +describe.skip('Packs - Create and Edit', { tags: ['@ess', '@serverless'] }, () => { let savedQueryId: string; let savedQueryName: string; let nomappingSavedQueryId: string; From 32a478ccc3663929add02a5c5b8b582ef3cbd428 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 8 Oct 2024 10:44:32 -0600 Subject: [PATCH 24/50] [ResposeOps] Gemini connector, remove bad variable reference (#195308) --- docs/settings/alert-action-settings.asciidoc | 2 +- .../components/schemas/gemini_secrets.yaml | 6 +-- .../connector_types/gemini/connector.test.tsx | 6 ++- .../connector_types/gemini/params.test.tsx | 30 +----------- .../connector_types/gemini/translations.ts | 2 +- .../server/connector_types/gemini/gemini.ts | 6 +-- .../translations/translations/fr-FR.json | 2 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- .../alerting_api_integration/common/config.ts | 1 + .../tests/actions/connector_types/gemini.ts | 48 +++++++++++-------- .../group2/tests/actions/index.ts | 1 + 12 files changed, 46 insertions(+), 62 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 23cb7ed59e21d7..18273319ecd08d 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -491,7 +491,7 @@ For an <>, specifies the AWS access key `xpack.actions.preconfigured..secrets.apikey`:: An API key secret that varies by connector: -`xpack.actions.preconfigured..secrets.credentialsJSON`:: +`xpack.actions.preconfigured..secrets.credentialsJson`:: For an <>, specifies the GCP service account credentials JSON file for authentication. + -- diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_secrets.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_secrets.yaml index 98e89e1ba373c2..01c02b5f01abfd 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_secrets.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/gemini_secrets.yaml @@ -2,8 +2,8 @@ title: Connector secrets properties for a Google Gemini connector description: Defines secrets for connectors when type is `.gemini`. type: object required: - - credentialsJSON + - credentialsJson properties: - credentialsJSON: + credentialsJson: type: string - description: The service account credentials JSON file. The service account should have Vertex AI user IAM role assigned to it. \ No newline at end of file + description: The service account credentials JSON file. The service account should have Vertex AI user IAM role assigned to it. diff --git a/x-pack/plugins/stack_connectors/public/connector_types/gemini/connector.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/gemini/connector.test.tsx index e470b4730f8702..48818281be0486 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/gemini/connector.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/gemini/connector.test.tsx @@ -37,7 +37,7 @@ const geminiConnector = { gcpProjectID: 'test-project', }, secrets: { - credentialsJSON: JSON.stringify({ + credentialsJson: JSON.stringify({ type: 'service_account', project_id: '', private_key_id: '', @@ -83,6 +83,10 @@ describe('GeminiConnectorFields renders', () => { ); expect(getAllByTestId('gemini-api-doc')[0]).toBeInTheDocument(); expect(getAllByTestId('gemini-api-model-doc')[0]).toBeInTheDocument(); + + expect(getAllByTestId('secrets.credentialsJson-input')[0]).toHaveValue( + geminiConnector.secrets.credentialsJson + ); }); describe('Dashboard link', () => { diff --git a/x-pack/plugins/stack_connectors/public/connector_types/gemini/params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/gemini/params.test.tsx index b8e3b3029b284f..1669603218c50c 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/gemini/params.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/gemini/params.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import GeminiParamsFields from './params'; -import { DEFAULT_GEMINI_URL, SUB_ACTION } from '../../../common/gemini/constants'; +import { SUB_ACTION } from '../../../common/gemini/constants'; import { I18nProvider } from '@kbn/i18n-react'; const messageVariables = [ @@ -48,37 +48,9 @@ describe('Gemini Params Fields renders', () => { }; const editAction = jest.fn(); const errors = {}; - const actionConnector = { - secrets: { - credentialsJSON: JSON.stringify({ - type: 'service_account', - project_id: '', - private_key_id: '', - private_key: '-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n', - client_email: '', - client_id: '', - auth_uri: 'https://accounts.google.com/o/oauth2/auth', - token_uri: 'https://oauth2.googleapis.com/token', - auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs', - client_x509_cert_url: '', - }), - }, - id: 'test', - actionTypeId: '.gemini', - isPreconfigured: false, - isSystemAction: false as const, - isDeprecated: false, - name: 'My Gemini Connector', - config: { - apiUrl: DEFAULT_GEMINI_URL, - gcpRegion: 'us-central-1', - gcpProjectID: 'test-project', - }, - }; render( { /** Retrieve access token based on the GCP service account credential json file */ private async getAccessToken(): Promise { // Validate the service account credentials JSON file input - let credentialsJSON; + let credentialsJson; try { - credentialsJSON = JSON.parse(this.secrets.credentialsJson); + credentialsJson = JSON.parse(this.secrets.credentialsJson); } catch (error) { throw new Error(`Failed to parse credentials JSON file: Invalid JSON format`); } const accessToken = await getGoogleOAuthJwtAccessToken({ connectorId: this.connector.id, logger: this.logger, - credentials: credentialsJSON, + credentials: credentialsJson, connectorTokenClient: this.connectorTokenClient, }); return accessToken; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c17b2fa3e66bee..d65dddae39479c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -43196,7 +43196,7 @@ "xpack.stackConnectors.components.gemini.bodyCodeEditorAriaLabel": "Éditeur de code", "xpack.stackConnectors.components.gemini.bodyFieldLabel": "Corps", "xpack.stackConnectors.components.gemini.connectorTypeTitle": "Google Gemini", - "xpack.stackConnectors.components.gemini.credentialsJSON": "Informations d'identification JSON", + "xpack.stackConnectors.components.gemini.credentialsJson": "Informations d'identification JSON", "xpack.stackConnectors.components.gemini.dashboardLink": "Affichez le tableau de bord d'utilisation de {apiProvider} pour le connecteur \"{ connectorName }\"", "xpack.stackConnectors.components.gemini.defaultModelTextFieldLabel": "Modèle par défaut", "xpack.stackConnectors.components.gemini.documentation": "documentation", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index df88db76dfaa50..3883a411648350 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -42935,7 +42935,7 @@ "xpack.stackConnectors.components.gemini.bodyCodeEditorAriaLabel": "コードエディター", "xpack.stackConnectors.components.gemini.bodyFieldLabel": "本文", "xpack.stackConnectors.components.gemini.connectorTypeTitle": "Google Gemini", - "xpack.stackConnectors.components.gemini.credentialsJSON": "資格情報JSON", + "xpack.stackConnectors.components.gemini.credentialsJson": "資格情報JSON", "xpack.stackConnectors.components.gemini.dashboardLink": "\"{ connectorName }\"コネクターの{apiProvider}使用状況ダッシュボードを表示", "xpack.stackConnectors.components.gemini.defaultModelTextFieldLabel": "デフォルトモデル", "xpack.stackConnectors.components.gemini.documentation": "ドキュメンテーション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f3b9f95ec6601e..823092906f7bad 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -42986,7 +42986,7 @@ "xpack.stackConnectors.components.gemini.bodyCodeEditorAriaLabel": "代码编辑器", "xpack.stackConnectors.components.gemini.bodyFieldLabel": "正文", "xpack.stackConnectors.components.gemini.connectorTypeTitle": "Google Gemini", - "xpack.stackConnectors.components.gemini.credentialsJSON": "凭据 JSON", + "xpack.stackConnectors.components.gemini.credentialsJson": "凭据 JSON", "xpack.stackConnectors.components.gemini.dashboardLink": "查看“{ connectorName }”连接器的 {apiProvider} 使用情况仪表板", "xpack.stackConnectors.components.gemini.defaultModelTextFieldLabel": "默认模型", "xpack.stackConnectors.components.gemini.documentation": "文档", diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 943135565428f3..fb0194b01be99a 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -40,6 +40,7 @@ const enabledActionTypes = [ '.bedrock', '.cases-webhook', '.email', + '.gemini', '.index', '.opsgenie', '.pagerduty', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts index 54eebf207e7d76..8d235c15dc21ce 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/gemini.ts @@ -11,6 +11,7 @@ import { geminiSuccessResponse, } from '@kbn/actions-simulators-plugin/server/gemini_simulation'; import { TaskErrorSource } from '@kbn/task-manager-plugin/common'; +import { DEFAULT_GEMINI_MODEL } from '@kbn/stack-connectors-plugin/common/gemini/constants'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; const connectorTypeId = '.gemini'; @@ -20,7 +21,7 @@ const defaultConfig = { gcpProjectID: 'test-project', }; const secrets = { - credentialsJSON: JSON.stringify({ + credentialsJson: JSON.stringify({ type: 'service_account', project_id: '', private_key_id: '', @@ -39,14 +40,14 @@ export default function geminiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const configService = getService('config'); - const createConnector = async (url: string) => { + const createConnector = async (apiUrl: string) => { const { body } = await supertest .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name, connector_type_id: connectorTypeId, - config: { ...defaultConfig, url }, + config: { ...defaultConfig, apiUrl }, secrets, }) .expect(200); @@ -62,10 +63,10 @@ export default function geminiTest({ getService }: FtrProviderContext) { config: configService.get('kbnTestServer.serverArgs'), }, }); - const config = { ...defaultConfig, url: '' }; + const config = { ...defaultConfig, apiUrl: '' }; before(async () => { - config.url = await simulator.start(); + config.apiUrl = await simulator.start(); }); after(() => { @@ -92,7 +93,10 @@ export default function geminiTest({ getService }: FtrProviderContext) { name, connector_type_id: connectorTypeId, is_missing_secrets: false, - config, + config: { + ...config, + defaultModel: DEFAULT_GEMINI_MODEL, + }, }); }); @@ -112,20 +116,20 @@ export default function geminiTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [url, gcpRegion, gcpProjectID]: expected value of type [string] but got [undefined]', + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', }); }); }); it('should return 400 Bad Request when creating the connector without the project id', async () => { - const testConfig = { gcpRegion: 'us-central-1', url: '' }; + const testConfig = { gcpRegion: 'us-central-1', apiUrl: 'https://url.co' }; await supertest .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name, connector_type_id: connectorTypeId, - testConfig, + config: testConfig, secrets, }) .expect(400) @@ -140,14 +144,14 @@ export default function geminiTest({ getService }: FtrProviderContext) { }); it('should return 400 Bad Request when creating the connector without the region', async () => { - const testConfig = { gcpProjectID: 'test-project', url: '' }; + const testConfig = { gcpProjectID: 'test-project', apiUrl: 'https://url.co' }; await supertest .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name, connector_type_id: connectorTypeId, - testConfig, + config: testConfig, secrets, }) .expect(400) @@ -169,7 +173,8 @@ export default function geminiTest({ getService }: FtrProviderContext) { name, connector_type_id: connectorTypeId, config: { - url: 'http://gemini.mynonexistent.com', + ...defaultConfig, + apiUrl: 'http://gemini.mynonexistent.com', }, secrets, }) @@ -179,7 +184,7 @@ export default function geminiTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: error validating url: target url "http://gemini.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', + 'error validating action type config: Error configuring Google Gemini action: Error: error validating url: target url "http://gemini.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', }); }); }); @@ -199,7 +204,7 @@ export default function geminiTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type secrets: [token]: expected value of type [string] but got [undefined]', + 'error validating action type secrets: [credentialsJson]: expected value of type [string] but got [undefined]', }); }); }); @@ -257,7 +262,7 @@ export default function geminiTest({ getService }: FtrProviderContext) { retry: true, message: 'an error occurred while running the action', errorSource: TaskErrorSource.FRAMEWORK, - service_message: `Sub action "invalidAction" is not registered. Connector id: ${geminiActionId}. Connector name: Gemini. Connector type: .gemini`, + service_message: `Sub action "invalidAction" is not registered. Connector id: ${geminiActionId}. Connector name: Google Gemini. Connector type: .gemini`, }); }); }); @@ -269,19 +274,21 @@ export default function geminiTest({ getService }: FtrProviderContext) { config: configService.get('kbnTestServer.serverArgs'), }, }); - let url: string; + let apiUrl: string; let geminiActionId: string; before(async () => { - url = await simulator.start(); - geminiActionId = await createConnector(url); + apiUrl = await simulator.start(); + geminiActionId = await createConnector(apiUrl); }); after(() => { simulator.close(); }); - it('should invoke AI with assistant AI body argument formatted to gemini expectations', async () => { + // TODO to fix, we need to figure out how to mock the gcp oauth token + // https://github.com/elastic/kibana/issues/195437 + it.skip('should invoke AI with assistant AI body argument formatted to gemini expectations', async () => { const { body } = await supertest .post(`/api/actions/connector/${geminiActionId}/_execute`) .set('kbn-xsrf', 'foo') @@ -289,7 +296,7 @@ export default function geminiTest({ getService }: FtrProviderContext) { params: { subAction: 'invokeAI', subActionParams: { - contents: [ + messages: [ { role: 'user', parts: [ @@ -315,7 +322,6 @@ export default function geminiTest({ getService }: FtrProviderContext) { ], }, ], - generation_config: { temperature: 0, maxOutputTokens: 8192 }, }, }, }) 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 2f773c7bbf43bf..fdfba7c5c2cf35 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 @@ -45,6 +45,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide loadTestFile(require.resolve('./connector_types/d3security')); loadTestFile(require.resolve('./connector_types/thehive')); loadTestFile(require.resolve('./connector_types/bedrock')); + loadTestFile(require.resolve('./connector_types/gemini')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./execute')); From 653073f8c6d199b069830c3acc13fb0c6664e9d8 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 8 Oct 2024 18:57:23 +0200 Subject: [PATCH 25/50] [Lens] Fix partition theme after ech upgrade (#195269) ## Summary When I've updated elastic-charts with the last new version, the `point.fill` parameter of lineSeriesStyle has changed in value. This created an unwanted change also in the partition chart that was using that style to color the partition sector borders. I've removed the useless color override of `sectorLineStroke` in the overwrite partition theme, leaving the control to use the chart theme. I've also removed the possibility of unwanted changes for other properties like `fontFamily` and in the `linkedText.textColor`. In the same PR I've removed duplicated tests that where testing exactly the same code/arguments/data, the only difference was the test name. I've also refactored a bit the code, cleaning up the typings and consolidating a bit the theme override logic --- .../partition_vis_component.test.tsx.snap | 24 -- .../components/partition_vis_component.tsx | 10 +- .../public/utils/get_partition_theme.test.ts | 297 +++++------------- .../public/utils/get_partition_theme.ts | 159 ++++------ 4 files changed, 153 insertions(+), 337 deletions(-) diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap index 35c1fc0e632131..e1293b9986f045 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap @@ -810,17 +810,13 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = ` "partition": Object { "circlePadding": 4, "emptySizeRatio": 0.3, - "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", "linkLabel": Object { - "fontSize": 11, "maxCount": 5, "maxTextLength": 100, - "textColor": "#343741", }, "maxFontSize": 16, "minFontSize": 10, "outerSizeRatio": undefined, - "sectorLineStroke": "__use__series__color__", "sectorLineWidth": 1.5, }, }, @@ -1742,17 +1738,13 @@ exports[`PartitionVisComponent should render correct structure for mosaic 1`] = "partition": Object { "circlePadding": 4, "emptySizeRatio": 0, - "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", "linkLabel": Object { - "fontSize": 11, "maxCount": 5, "maxTextLength": undefined, - "textColor": "#343741", }, "maxFontSize": 16, "minFontSize": 10, "outerSizeRatio": 1, - "sectorLineStroke": "__use__series__color__", "sectorLineWidth": 1.5, }, }, @@ -2738,17 +2730,13 @@ exports[`PartitionVisComponent should render correct structure for multi-metric "partition": Object { "circlePadding": 4, "emptySizeRatio": 0, - "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", "linkLabel": Object { - "fontSize": 11, "maxCount": 5, "maxTextLength": 100, - "textColor": "#343741", }, "maxFontSize": 16, "minFontSize": 10, "outerSizeRatio": undefined, - "sectorLineStroke": "__use__series__color__", "sectorLineWidth": 1.5, }, }, @@ -3734,17 +3722,13 @@ exports[`PartitionVisComponent should render correct structure for pie 1`] = ` "partition": Object { "circlePadding": 4, "emptySizeRatio": 0, - "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", "linkLabel": Object { - "fontSize": 11, "maxCount": 5, "maxTextLength": 100, - "textColor": "#343741", }, "maxFontSize": 16, "minFontSize": 10, "outerSizeRatio": undefined, - "sectorLineStroke": "__use__series__color__", "sectorLineWidth": 1.5, }, }, @@ -4666,17 +4650,13 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] = "partition": Object { "circlePadding": 4, "emptySizeRatio": 0, - "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", "linkLabel": Object { - "fontSize": 11, "maxCount": 5, "maxTextLength": undefined, - "textColor": "#343741", }, "maxFontSize": 16, "minFontSize": 10, "outerSizeRatio": 1, - "sectorLineStroke": "__use__series__color__", "sectorLineWidth": 1.5, }, }, @@ -5562,17 +5542,13 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] = "partition": Object { "circlePadding": 4, "emptySizeRatio": 0, - "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", "linkLabel": Object { - "fontSize": 11, "maxCount": 5, "maxTextLength": undefined, - "textColor": "#343741", }, "maxFontSize": 16, "minFontSize": 10, "outerSizeRatio": undefined, - "sectorLineStroke": "__use__series__color__", "sectorLineWidth": 1.5, }, }, diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index 826e868cab5401..5baf582877a68a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -378,19 +378,11 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { getPartitionTheme( visType, visParams, - chartBaseTheme, containerDimensions, rescaleFactor, hasOpenedOnAggBasedEditor ), - [ - visType, - visParams, - chartBaseTheme, - containerDimensions, - rescaleFactor, - hasOpenedOnAggBasedEditor, - ] + [visType, visParams, containerDimensions, rescaleFactor, hasOpenedOnAggBasedEditor] ); const fixedViewPort = document.getElementById('app-fixed-viewport'); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts index 26508151e9c59f..e6cfeabb5e70ca 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts @@ -11,8 +11,6 @@ import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { getPartitionTheme } from './get_partition_theme'; import { createMockPieParams, createMockDonutParams, createMockPartitionVisParams } from '../mocks'; import { ChartTypes, LabelPositions, PartitionVisParams } from '../../common/types'; -import { RecursivePartial } from '@elastic/eui'; -import { Theme } from '@elastic/charts'; const column: ExpressionValueVisDimension = { type: 'vis_dimension', @@ -29,11 +27,6 @@ const column: ExpressionValueVisDimension = { const splitRows = [column]; const splitColumns = [column]; -const chartTheme: RecursivePartial = { - barSeriesStyle: { displayValue: { fontFamily: 'Arial' } }, - lineSeriesStyle: { point: { fill: '#fff' } }, - axes: { axisTitle: { fill: '#000' } }, -}; const linkLabelWithEnoughSpace = (visParams: PartitionVisParams) => ({ maxCount: Number.POSITIVE_INFINITY, @@ -41,36 +34,33 @@ const linkLabelWithEnoughSpace = (visParams: PartitionVisParams) => ({ maxTextLength: visParams.labels.truncate ?? undefined, }); -const linkLabelsWithoutSpaceForOuterLabels = { maxCount: 0 }; +const linkLabelsWithoutSpaceForOuterLabels = (visParams: PartitionVisParams) => ({ + maxCount: 0, + maxTextLength: visParams.labels.truncate ?? undefined, +}); -const linkLabelsWithoutSpaceForLabels = { +const linkLabelsWithoutSpaceForLabels = (visParams: PartitionVisParams) => ({ maxCount: 0, maximumSection: Number.POSITIVE_INFINITY, -}; + maxTextLength: visParams.labels.truncate ?? undefined, +}); -const getStaticThemePartition = ( - theme: RecursivePartial, - visParams: PartitionVisParams -) => ({ - fontFamily: theme.barSeriesStyle?.displayValue?.fontFamily, +const getStaticThemePartition = (visParams: PartitionVisParams) => ({ outerSizeRatio: 1, minFontSize: 10, maxFontSize: 16, emptySizeRatio: visParams.emptySizeRatio ?? 0, - sectorLineStroke: theme.lineSeriesStyle?.point?.fill, sectorLineWidth: 1.5, circlePadding: 4, }); -const getStaticThemeOptions = (theme: RecursivePartial, visParams: PartitionVisParams) => ({ - partition: getStaticThemePartition(theme, visParams), +const getStaticThemeOptions = (visParams: PartitionVisParams) => ({ + partition: getStaticThemePartition(visParams), chartMargins: { top: 0, left: 0, bottom: 0, right: 0 }, }); -const getDefaultLinkLabel = (visParams: PartitionVisParams, theme: RecursivePartial) => ({ +const getDefaultLinkLabel = (visParams: PartitionVisParams) => ({ maxCount: 5, - fontSize: 11, - textColor: theme.axes?.axisTitle?.fill, maxTextLength: visParams.labels.truncate ?? undefined, }); @@ -86,165 +76,99 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition dimensions: { ...visParams.dimensions, splitColumn: splitColumns }, }; - it('should return correct default theme options', () => { - const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); - expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), - partition: { - ...getStaticThemePartition(chartTheme, visParams), - outerSizeRatio: undefined, - linkLabel: getDefaultLinkLabel(visParams, chartTheme), - }, - }); - }); - - it('should not return padding settings if dimensions are not specified', () => { - const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); - - expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), - partition: { - ...getStaticThemePartition(chartTheme, visParams), - outerSizeRatio: undefined, - linkLabel: getDefaultLinkLabel(visParams, chartTheme), - }, - }); - }); - it('should not return padding settings if split column or row are specified', () => { - const themeForSplitColumns = getPartitionTheme( - chartType, - vParamsSplitColumns, - chartTheme, - dimensions - ); + const themeForSplitColumns = getPartitionTheme(chartType, vParamsSplitColumns, dimensions); expect(themeForSplitColumns).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + ...getStaticThemeOptions(vParamsSplitColumns), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitColumns), + ...getStaticThemePartition(vParamsSplitColumns), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParamsSplitColumns), }, }); - const themeForSplitRows = getPartitionTheme( - chartType, - vParamsSplitRows, - chartTheme, - dimensions - ); + const themeForSplitRows = getPartitionTheme(chartType, vParamsSplitRows, dimensions); expect(themeForSplitRows).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + ...getStaticThemeOptions(vParamsSplitRows), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitRows), + ...getStaticThemePartition(vParamsSplitRows), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParamsSplitRows), }, }); }); it('should return adjusted padding settings if dimensions are specified and is on aggBased editor', () => { const specifiedDimensions = { width: 2000, height: 2000 }; - const theme = getPartitionTheme( - chartType, - visParams, - chartTheme, - specifiedDimensions, - undefined, - true - ); + const theme = getPartitionTheme(chartType, visParams, specifiedDimensions, undefined, true); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), + ...getStaticThemeOptions(visParams), chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 }, partition: { - ...getStaticThemePartition(chartTheme, visParams), - linkLabel: getDefaultLinkLabel(visParams, chartTheme), + ...getStaticThemePartition(visParams), + linkLabel: getDefaultLinkLabel(visParams), }, }); }); it('should return right settings for the theme related fields', () => { - const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + const theme = getPartitionTheme(chartType, visParams, dimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), + ...getStaticThemeOptions(visParams), partition: { - ...getStaticThemePartition(chartTheme, visParams), + ...getStaticThemePartition(visParams), outerSizeRatio: undefined, - linkLabel: getDefaultLinkLabel(visParams, chartTheme), + linkLabel: getDefaultLinkLabel(visParams), }, }); }); it('should return undefined outerSizeRatio for split chart and show labels', () => { const specifiedDimensions = { width: 2000, height: 2000 }; - const theme = getPartitionTheme(chartType, vParamsSplitRows, chartTheme, specifiedDimensions); + const theme = getPartitionTheme(chartType, vParamsSplitRows, specifiedDimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + ...getStaticThemeOptions(vParamsSplitRows), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitRows), + ...getStaticThemePartition(vParamsSplitRows), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParamsSplitRows), }, }); const themeForSplitColumns = getPartitionTheme( chartType, vParamsSplitColumns, - chartTheme, specifiedDimensions ); expect(themeForSplitColumns).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + ...getStaticThemeOptions(vParamsSplitColumns), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitColumns), + ...getStaticThemePartition(vParamsSplitColumns), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParamsSplitColumns), }, }); }); - it( - 'should return undefined outerSizeRatio for not specified dimensions, visible labels,' + - 'and default labels position and not split chart', - () => { - const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); - - expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), - partition: { - ...getStaticThemePartition(chartTheme, visParams), - outerSizeRatio: undefined, - linkLabel: getDefaultLinkLabel(visParams, chartTheme), - }, - }); - } - ); - it( 'should return rescaleFactor value for outerSizeRatio if dimensions are specified,' + ' is not split chart, labels are shown and labels position is not `inside`', () => { const specifiedDimensions = { width: 2000, height: 2000 }; const rescaleFactor = 2; - const theme = getPartitionTheme( - chartType, - visParams, - chartTheme, - specifiedDimensions, - rescaleFactor - ); + const theme = getPartitionTheme(chartType, visParams, specifiedDimensions, rescaleFactor); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), + ...getStaticThemeOptions(visParams), partition: { - ...getStaticThemePartition(chartTheme, visParams), + ...getStaticThemePartition(visParams), outerSizeRatio: rescaleFactor, - linkLabel: getDefaultLinkLabel(visParams, chartTheme), + linkLabel: getDefaultLinkLabel(visParams), }, }); } @@ -260,20 +184,14 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition labels: { ...visParams.labels, position: LabelPositions.INSIDE }, }; - const theme = getPartitionTheme( - chartType, - vParams, - chartTheme, - specifiedDimensions, - rescaleFactor - ); + const theme = getPartitionTheme(chartType, vParams, specifiedDimensions, rescaleFactor); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, vParams), + ...getStaticThemeOptions(vParams), partition: { - ...getStaticThemePartition(chartTheme, vParams), + ...getStaticThemePartition(vParams), outerSizeRatio: 0.5, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParams), }, }); } @@ -287,12 +205,12 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition ...visParams, labels: { ...visParams.labels, last_level: true }, }; - const theme = getPartitionTheme(chartType, vParams, chartTheme, specifiedDimensions); + const theme = getPartitionTheme(chartType, vParams, specifiedDimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, vParams), + ...getStaticThemeOptions(vParams), partition: { - ...getStaticThemePartition(chartTheme, vParams), + ...getStaticThemePartition(vParams), linkLabel: linkLabelWithEnoughSpace(vParams), }, }); @@ -304,55 +222,50 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition ...visParams, labels: { ...visParams.labels, position: LabelPositions.INSIDE }, }; - const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions); + const theme = getPartitionTheme(chartType, vParams, dimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, vParams), + ...getStaticThemeOptions(vParams), partition: { - ...getStaticThemePartition(chartTheme, vParams), + ...getStaticThemePartition(vParams), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParams), }, }); - const themeSplitColumns = getPartitionTheme( - chartType, - vParamsSplitColumns, - chartTheme, - dimensions - ); + const themeSplitColumns = getPartitionTheme(chartType, vParamsSplitColumns, dimensions); expect(themeSplitColumns).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + ...getStaticThemeOptions(vParamsSplitColumns), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitColumns), + ...getStaticThemePartition(vParamsSplitColumns), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParamsSplitColumns), }, }); - const themeSplitRows = getPartitionTheme(chartType, vParamsSplitRows, chartTheme, dimensions); + const themeSplitRows = getPartitionTheme(chartType, vParamsSplitRows, dimensions); expect(themeSplitRows).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + ...getStaticThemeOptions(vParamsSplitRows), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitRows), + ...getStaticThemePartition(vParamsSplitRows), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForOuterLabels, + linkLabel: linkLabelsWithoutSpaceForOuterLabels(vParamsSplitRows), }, }); }); it('should hide links if labels are not shown', () => { const vParams = { ...visParams, labels: { ...visParams.labels, show: false } }; - const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions); + const theme = getPartitionTheme(chartType, vParams, dimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, vParams), + ...getStaticThemeOptions(vParams), partition: { - ...getStaticThemePartition(chartTheme, vParams), + ...getStaticThemePartition(vParams), outerSizeRatio: undefined, - linkLabel: linkLabelsWithoutSpaceForLabels, + linkLabel: linkLabelsWithoutSpaceForLabels(vParams), }, }); }); @@ -369,101 +282,61 @@ const runTreemapMosaicTestSuites = (chartType: ChartTypes, visParams: PartitionV }; it('should return correct theme options', () => { - const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); - expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), - partition: { - ...getStaticThemePartition(chartTheme, visParams), - linkLabel: getDefaultLinkLabel(visParams, chartTheme), - }, - }); - }); - - it('should return empty padding settings if dimensions are not specified', () => { - const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); - + const theme = getPartitionTheme(chartType, visParams, dimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), + ...getStaticThemeOptions(visParams), partition: { - ...getStaticThemePartition(chartTheme, visParams), - linkLabel: getDefaultLinkLabel(visParams, chartTheme), + ...getStaticThemePartition(visParams), + linkLabel: getDefaultLinkLabel(visParams), }, }); }); it('should return padding settings if split column or row are specified', () => { - const themeForSplitColumns = getPartitionTheme( - chartType, - vParamsSplitColumns, - chartTheme, - dimensions - ); + const themeForSplitColumns = getPartitionTheme(chartType, vParamsSplitColumns, dimensions); expect(themeForSplitColumns).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + ...getStaticThemeOptions(vParamsSplitColumns), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitColumns), - linkLabel: getDefaultLinkLabel(vParamsSplitColumns, chartTheme), + ...getStaticThemePartition(vParamsSplitColumns), + linkLabel: getDefaultLinkLabel(vParamsSplitColumns), }, }); - const themeForSplitRows = getPartitionTheme( - chartType, - vParamsSplitRows, - chartTheme, - dimensions - ); + const themeForSplitRows = getPartitionTheme(chartType, vParamsSplitRows, dimensions); expect(themeForSplitRows).toEqual({ - ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + ...getStaticThemeOptions(vParamsSplitRows), partition: { - ...getStaticThemePartition(chartTheme, vParamsSplitRows), - linkLabel: getDefaultLinkLabel(vParamsSplitRows, chartTheme), + ...getStaticThemePartition(vParamsSplitRows), + linkLabel: getDefaultLinkLabel(vParamsSplitRows), }, }); }); it('should return fullfilled padding settings if dimensions are specified', () => { const specifiedDimensions = { width: 2000, height: 2000 }; - const theme = getPartitionTheme( - chartType, - visParams, - chartTheme, - specifiedDimensions, - undefined, - true - ); + const theme = getPartitionTheme(chartType, visParams, specifiedDimensions, undefined, true); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), + ...getStaticThemeOptions(visParams), chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 }, partition: { - ...getStaticThemePartition(chartTheme, visParams), - linkLabel: getDefaultLinkLabel(visParams, chartTheme), - }, - }); - }); - - it('should return settings for the theme related fields', () => { - const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); - expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), - partition: { - ...getStaticThemePartition(chartTheme, visParams), - linkLabel: getDefaultLinkLabel(visParams, chartTheme), + ...getStaticThemePartition(visParams), + linkLabel: getDefaultLinkLabel(visParams), }, }); }); it('should make color transparent if labels are hidden', () => { const vParams = { ...visParams, labels: { ...visParams.labels, show: false } }; - const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions); + const theme = getPartitionTheme(chartType, vParams, dimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, vParams), + ...getStaticThemeOptions(vParams), partition: { - ...getStaticThemePartition(chartTheme, vParams), - linkLabel: getDefaultLinkLabel(visParams, chartTheme), + ...getStaticThemePartition(vParams), + linkLabel: getDefaultLinkLabel(visParams), fillLabel: { textColor: 'rgba(0,0,0,0)' }, }, }); @@ -481,14 +354,14 @@ describe('Donut getPartitionTheme', () => { runPieDonutWaffleTestSuites(chartType, visParams); it('should return correct empty size ratio and partitionLayout', () => { - const theme = getPartitionTheme(ChartTypes.DONUT, visParams, chartTheme, dimensions); + const theme = getPartitionTheme(ChartTypes.DONUT, visParams, dimensions); expect(theme).toEqual({ - ...getStaticThemeOptions(chartTheme, visParams), + ...getStaticThemeOptions(visParams), outerSizeRatio: undefined, partition: { - ...getStaticThemePartition(chartTheme, visParams), - linkLabel: getDefaultLinkLabel(visParams, chartTheme), + ...getStaticThemePartition(visParams), + linkLabel: getDefaultLinkLabel(visParams), outerSizeRatio: undefined, }, }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts index e1e47dc27fc5b9..8ee92d31544b51 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { RecursivePartial, Theme, PartialTheme } from '@elastic/charts'; +import { PartialTheme } from '@elastic/charts'; import { ChartTypes, LabelPositions, @@ -15,37 +15,13 @@ import { PieContainerDimensions, } from '../../common/types'; -type GetThemeByTypeFn = ( - chartType: ChartTypes, - visParams: PartitionVisParams, - dimensions?: PieContainerDimensions, - rescaleFactor?: number -) => PartialTheme; - -type GetThemeFn = ( - chartType: ChartTypes, - visParams: PartitionVisParams, - chartTheme: RecursivePartial, - dimensions?: PieContainerDimensions, - rescaleFactor?: number, - hasOpenedOnAggBasedEditor?: boolean -) => PartialTheme; +const MAX_SIZE = 1000; -type GetPieDonutWaffleThemeFn = ( +function getPieDonutWaffleCommonTheme( visParams: PartitionVisParams, dimensions?: PieContainerDimensions, - rescaleFactor?: number -) => PartialTheme; - -type GetTreemapMosaicThemeFn = (visParams: PartitionVisParams) => PartialTheme; - -const MAX_SIZE = 1000; - -const getPieDonutWaffleCommonTheme: GetPieDonutWaffleThemeFn = ( - visParams, - dimensions, rescaleFactor = 1 -) => { +): PartialTheme['partition'] { const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); const preventLinksFromShowing = (visParams.labels.position === LabelPositions.INSIDE || isSplitChart) && visParams.labels.show; @@ -59,15 +35,14 @@ const getPieDonutWaffleCommonTheme: GetPieDonutWaffleThemeFn = ( } : { outerSizeRatio: undefined }; - const theme: PartialTheme = {}; - theme.partition = { ...(usingOuterSizeRatio ?? {}) }; + const partitionTheme: PartialTheme['partition'] = { ...(usingOuterSizeRatio ?? {}) }; if ( visParams.labels.show && visParams.labels.position === LabelPositions.DEFAULT && visParams.labels.last_level ) { - theme.partition.linkLabel = { + partitionTheme.linkLabel = { maxCount: Number.POSITIVE_INFINITY, maximumSection: Number.POSITIVE_INFINITY, maxTextLength: visParams.labels.truncate ?? undefined, @@ -76,7 +51,7 @@ const getPieDonutWaffleCommonTheme: GetPieDonutWaffleThemeFn = ( if (preventLinksFromShowing || !visParams.labels.show) { // Prevent links from showing - theme.partition.linkLabel = { + partitionTheme.linkLabel = { maxCount: 0, ...(!visParams.labels.show ? { maximumSection: Number.POSITIVE_INFINITY } : {}), }; @@ -84,48 +59,55 @@ const getPieDonutWaffleCommonTheme: GetPieDonutWaffleThemeFn = ( if (!preventLinksFromShowing && dimensions && !isSplitChart) { // shrink up to 20% to give some room for the linked values - theme.partition.outerSizeRatio = rescaleFactor; + partitionTheme.outerSizeRatio = rescaleFactor; } - return theme; -}; + return partitionTheme; +} + +function getDonutSpecificTheme( + visParams: PartitionVisParams, + dimensions?: PieContainerDimensions, + rescaleFactor?: number +): PartialTheme['partition'] { + const partition = getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor); + return { ...partition, emptySizeRatio: visParams.emptySizeRatio }; +} -const getDonutSpecificTheme: GetPieDonutWaffleThemeFn = (visParams, ...args) => { - const { partition, ...restTheme } = getPieDonutWaffleCommonTheme(visParams, ...args); - return { ...restTheme, partition: { ...partition, emptySizeRatio: visParams.emptySizeRatio } }; -}; +function getTreemapMosaicCommonTheme(visParams: PartitionVisParams): PartialTheme['partition'] { + return !visParams.labels.show ? { fillLabel: { textColor: 'rgba(0,0,0,0)' } } : {}; +} -const getTreemapMosaicCommonTheme: GetTreemapMosaicThemeFn = (visParams) => { - if (!visParams.labels.show) { - return { - partition: { - fillLabel: { textColor: 'rgba(0,0,0,0)' }, - }, - }; +function getSpecificTheme( + chartType: ChartTypes, + visParams: PartitionVisParams, + dimensions?: PieContainerDimensions, + rescaleFactor?: number +): PartialTheme['partition'] { + switch (chartType) { + case ChartTypes.PIE: + return getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor); + case ChartTypes.DONUT: + return getDonutSpecificTheme(visParams, dimensions, rescaleFactor); + case ChartTypes.TREEMAP: + return getTreemapMosaicCommonTheme(visParams); + case ChartTypes.MOSAIC: + return getTreemapMosaicCommonTheme(visParams); + case ChartTypes.WAFFLE: + return getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor); } - return {}; -}; +} -const getSpecificTheme: GetThemeByTypeFn = (chartType, visParams, dimensions, rescaleFactor) => - ({ - [ChartTypes.PIE]: () => getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor), - [ChartTypes.DONUT]: () => getDonutSpecificTheme(visParams, dimensions, rescaleFactor), - [ChartTypes.TREEMAP]: () => getTreemapMosaicCommonTheme(visParams), - [ChartTypes.MOSAIC]: () => getTreemapMosaicCommonTheme(visParams), - [ChartTypes.WAFFLE]: () => getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor), - }[chartType]()); - -export const getPartitionTheme: GetThemeFn = ( - chartType, - visParams, - chartTheme, - dimensions, +export function getPartitionTheme( + chartType: ChartTypes, + visParams: PartitionVisParams, + dimensions?: PieContainerDimensions, rescaleFactor = 1, - hasOpenedOnAggBasedEditor -) => { + hasOpenedOnAggBasedEditor?: boolean +): PartialTheme { // On small multiples we want the labels to only appear inside const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); - const paddingProps: PartialTheme | null = + const paddingProps: PartialTheme = dimensions && !isSplitChart && hasOpenedOnAggBasedEditor ? { chartPaddings: { @@ -135,34 +117,27 @@ export const getPartitionTheme: GetThemeFn = ( right: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height, }, } - : null; - const partition = { - fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily, - outerSizeRatio: 1, - minFontSize: 10, - maxFontSize: 16, - emptySizeRatio: 0, - sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill, - sectorLineWidth: 1.5, - circlePadding: 4, - linkLabel: { - maxCount: 5, - fontSize: 11, - textColor: chartTheme.axes?.axisTitle?.fill, - maxTextLength: visParams.labels.truncate ?? undefined, - }, - }; - const { partition: specificPartition = {}, ...restSpecificTheme } = getSpecificTheme( - chartType, - visParams, - dimensions, - rescaleFactor - ); + : {}; + + const specificPartition = getSpecificTheme(chartType, visParams, dimensions, rescaleFactor); return { - partition: { ...partition, ...specificPartition }, + partition: { + outerSizeRatio: 1, + minFontSize: 10, + maxFontSize: 16, + emptySizeRatio: 0, + sectorLineWidth: 1.5, + circlePadding: 4, + ...specificPartition, + linkLabel: { + // fontSize: 11, + maxTextLength: visParams.labels.truncate ?? undefined, + maxCount: 5, + ...specificPartition?.linkLabel, + }, + }, chartMargins: { top: 0, bottom: 0, left: 0, right: 0 }, - ...(paddingProps ?? {}), - ...restSpecificTheme, + ...paddingProps, }; -}; +} From a78a31d1873a7dca3d175870aee05801b056f5a4 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 8 Oct 2024 20:04:40 +0300 Subject: [PATCH 26/50] fix: [Obs Applications > Services | Traces | Dependencies][SCREEN READER]: H1 tag should not include secondary information (#193880) Closes: https://github.com/elastic/kibana/issues/194988 Closes: https://github.com/elastic/kibana/issues/194987 Closes: https://github.com/elastic/kibana/issues/194986 ## Description Observability has a few pages that wrap related information like alert counts in the H1 tag. This presents a challenge to screen readers because all of that information now becomes the heading level one. It clogs up the Headings menu and makes it harder to reason about the page and what's primary information vs. secondary. ## What was changed?: 1. extra content has been removed from `pageTitle` and moved to `rightSideItems`. ## Screen: image > [!NOTE] > On smaller screens (at certain resolutions) sometimes we have an issue described in https://github.com/elastic/eui/issues/8039 . But It's not a blocker for that PR and will be fixed on EUI side --- .../templates/apm_main_template/index.tsx | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx index e536ed94568015..f4ef2044a38c75 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderProps } from '@elastic/eui'; +import { EuiFlexGroup, EuiPageHeaderProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { entityCentricExperience } from '@kbn/observability-plugin/common'; import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; @@ -132,35 +132,24 @@ export function ApmMainTemplate({ noDataConfig, }); - const rightSideItems = [...(showServiceGroupSaveButton ? [] : [])]; - const sanitizedPath = getPathForFeedback(window.location.pathname); - const pageHeaderTitle = ( - - {pageHeader?.pageTitle ?? pageTitle} - - - - - - - {environmentFilter && } - - - - ); + const rightSideItems = [ + ...(showServiceGroupSaveButton ? [] : []), + ...(environmentFilter ? [] : []), + , + ]; const [dismissedEntitiesInventoryCallout, setdismissedEntitiesInventoryCallout] = useLocalStorage( `apm.dismissedEntitiesInventoryCallout`, @@ -180,7 +169,7 @@ export function ApmMainTemplate({ pageHeader={{ rightSideItems, ...pageHeader, - pageTitle: pageHeaderTitle, + pageTitle: pageHeader?.pageTitle ?? pageTitle, children: ( {showEntitiesInventoryCallout ? ( From d583ddf41ac41f09fd97fbd6b91c2d7076333d97 Mon Sep 17 00:00:00 2001 From: Linghao Su Date: Wed, 9 Oct 2024 01:28:57 +0800 Subject: [PATCH 27/50] [Vega] Fix tooltip position on faceted charts (#194620) Fixes #163815 where the tooltip was incorrectly positioned on faceted Vega charts --- .../vega/public/vega_view/vega_tooltip.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_types/vega/public/vega_view/vega_tooltip.js index e12316c242d776..306d00a145c150 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_tooltip.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_tooltip.js @@ -79,12 +79,19 @@ export class TooltipHandler { anchorBounds = createRect(event.clientX, event.clientY, 0, 0); } else { const containerBox = this.container.getBoundingClientRect(); - anchorBounds = createRect( - containerBox.left + view._origin[0] + item.bounds.x1, - containerBox.top + view._origin[1] + item.bounds.y1, - item.bounds.width(), - item.bounds.height() - ); + let left = containerBox.left + view._origin[0] + item.bounds.x1; + let top = containerBox.top + view._origin[1] + item.bounds.y1; + + // loop item mark group + let ancestorItem = item; + + while (ancestorItem.mark.group) { + ancestorItem = ancestorItem.mark.group; + left += ancestorItem.x; + top += ancestorItem.y; + } + + anchorBounds = createRect(left, top, item.bounds.width(), item.bounds.height()); } const pos = calculatePopoverPosition( From bf621693f20b012593ec5fcb84c7386da952aeb3 Mon Sep 17 00:00:00 2001 From: Jedr Blaszyk Date: Tue, 8 Oct 2024 19:30:46 +0200 Subject: [PATCH 28/50] [Search] Fix issue with crawler not getting deleted (#195440) ## Summary The bug is that connector doc can be of `elastic-crawler` service type, we forgot about this in the logic that handles detaching the index from connector upon index delation. This change checks if a connector doc, matching the `index_name` to be deleted, is of crawler type: - If yes, delete the connector (crawler) doc, as crawler is always tied 1:1 to an index - If no, detach the index, leave the connector doc in the connector index This bug was likely introduced as a part of: https://github.com/elastic/kibana/pull/183833 (some lazy engineer forgot to test for this edge case ...) ## Validation ### Delete Crawler case 1: Delete Crawler deletes crawler doc + related index https://github.com/user-attachments/assets/68ad14f7-4a7f-408c-8731-6ed0465f9ef1 ### Delete Crawler case 2: Delete crawler-related index deletes crawler doc https://github.com/user-attachments/assets/e2995697-32c4-4f8f-90ce-9c06c7e6d208 --- .../routes/enterprise_search/indices.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts index 78d7d2076438e1..70b46205c5e88b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts @@ -14,7 +14,12 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { deleteConnectorSecret, updateConnectorIndexName } from '@kbn/search-connectors'; +import { + CRAWLER_SERVICE_TYPE, + deleteConnectorSecret, + deleteConnectorById, + updateConnectorIndexName, +} from '@kbn/search-connectors'; import { fetchConnectorByIndexName, fetchConnectors, @@ -207,13 +212,17 @@ export function registerIndexRoutes({ } if (connector) { - // detach the deleted index without removing the connector - await updateConnectorIndexName(client.asCurrentUser, connector.id, null); - if (connector.api_key_id) { - await client.asCurrentUser.security.invalidateApiKey({ ids: [connector.api_key_id] }); - } - if (connector.api_key_secret_id) { - await deleteConnectorSecret(client.asCurrentUser, connector.api_key_secret_id); + if (connector.service_type === CRAWLER_SERVICE_TYPE) { + await deleteConnectorById(client.asCurrentUser, connector.id); + } else { + // detach the deleted index without removing the connector + await updateConnectorIndexName(client.asCurrentUser, connector.id, null); + if (connector.api_key_id) { + await client.asCurrentUser.security.invalidateApiKey({ ids: [connector.api_key_id] }); + } + if (connector.api_key_secret_id) { + await deleteConnectorSecret(client.asCurrentUser, connector.api_key_secret_id); + } } } From f892d31285f7879e3374b5dfa55be103271af2de Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 04:36:20 +1100 Subject: [PATCH 29/50] skip failing test suite (#195453) --- x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts index b6c210ece2ddc9..d74bd2483f7460 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts @@ -43,6 +43,7 @@ import { import { ServerlessRoleName } from '../../support/roles'; import { getAdvancedButton } from '../../screens/integrations'; +// Failing: See https://github.com/elastic/kibana/issues/195453 // Failing: See https://github.com/elastic/kibana/issues/195453 describe.skip('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { let caseId: string; From 8c7d22c0af40ca5772bc9c97610817600a5596f2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 04:36:32 +1100 Subject: [PATCH 30/50] skip failing test suite (#195476) --- .../management/cypress/e2e/endpoint_list/endpoints.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts index 12cdfcfa6e09cf..03b9797abb56ed 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints.cy.ts @@ -28,7 +28,8 @@ import { createEndpointHost } from '../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data'; import { enableAllPolicyProtections } from '../../tasks/endpoint_policy'; -describe('Endpoints page', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/195476 +describe.skip('Endpoints page', { tags: ['@ess', '@serverless'] }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; From 93c82265fbeaf8e0b19c818dd295251781a686ce Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 9 Oct 2024 04:36:49 +1100 Subject: [PATCH 31/50] skip failing test suite (#171435) --- x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts index 6f551ad39b1967..cafc97cb646ea5 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts @@ -19,7 +19,8 @@ import { LIVE_QUERY_EDITOR } from '../../screens/live_query'; import { getAdvancedButton } from '../../screens/integrations'; import { ServerlessRoleName } from '../../support/roles'; -describe('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/171435 +describe.skip('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); navigateTo('/app/osquery'); From 2bb8c6326645603aedd721d85463a4b32f2cdc6d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 18:37:32 +0100 Subject: [PATCH 32/50] skip flaky suite (#195458) --- x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts index c74b253ae9d41c..f0a19907d57d89 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/live_query_run.cy.ts @@ -23,7 +23,8 @@ import { getAdvancedButton } from '../../screens/integrations'; import { loadSavedQuery, cleanupSavedQuery } from '../../tasks/api_fixtures'; import { ServerlessRoleName } from '../../support/roles'; -describe('ALL - Live Query run custom and saved', { tags: ['@ess', '@serverless'] }, () => { +// FLAKY: https://github.com/elastic/kibana/issues/195458 +describe.skip('ALL - Live Query run custom and saved', { tags: ['@ess', '@serverless'] }, () => { let savedQueryId: string; let savedQueryName: string; From 16cd4bb1fed3bb4d4959b3162e873891dfab4b92 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 8 Oct 2024 19:46:43 +0200 Subject: [PATCH 33/50] [Security Solution] skips Flaky test (#195435) ## Summary Skips Flaky test : https://github.com/elastic/kibana/issues/189794 --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 7a6c23279f4358..9d450f46f4b012 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -901,7 +901,8 @@ describe('query tab with unified timeline', () => { ); }); - it( + // Flaky: https://github.com/elastic/kibana/issues/189794 + it.skip( 'should have the notification dot & correct tooltip', async () => { renderTestComponents(); From 9c8f689aca23ed8b1f560c57a9a660d318375412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 8 Oct 2024 13:48:07 -0400 Subject: [PATCH 34/50] Put the auto calculation of capacity behind a feature flag, for now (#195390) In this PR, I'm preparing for the 8.16 release where we'd like to start rolling out the `mget` task claiming strategy separately from the added concurrency. To accomplish this, we need to put the capacity calculation behind a feature flag that is default to false for now, until we do a second rollout with an increased concurrency. The increased concurrency can be calculated and adjusted based on experiments of clusters setting `xpack.task_manager.capacity` to a higher value and observe the resource usage. PR to deploy to Cloud and verify that we always default to 10 normal tasks: https://github.com/elastic/kibana/pull/195392 --- .../resources/base/bin/kibana-docker | 1 + .../task_manager/server/config.test.ts | 3 + x-pack/plugins/task_manager/server/config.ts | 1 + .../server/ephemeral_task_lifecycle.test.ts | 1 + .../managed_configuration.test.ts | 3 + .../lib/calculate_health_status.test.ts | 1 + .../server/lib/get_default_capacity.test.ts | 62 +++++++++++++++++++ .../server/lib/get_default_capacity.ts | 9 ++- .../server/metrics/create_aggregator.test.ts | 1 + .../configuration_statistics.test.ts | 1 + .../task_manager/server/plugin.test.ts | 1 + x-pack/plugins/task_manager/server/plugin.ts | 5 +- .../server/polling_lifecycle.test.ts | 1 + 13 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 110ca72895e86f..3c1e7ebe857faf 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -416,6 +416,7 @@ kibana_vars=( xpack.spaces.maxSpaces xpack.task_manager.capacity xpack.task_manager.claim_strategy + xpack.task_manager.auto_calculate_default_ech_capacity xpack.task_manager.discovery.active_nodes_lookback xpack.task_manager.discovery.interval xpack.task_manager.kibanas_per_partition diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index fa8c18207a6927..34dd5f1c6fbffe 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -13,6 +13,7 @@ describe('config validation', () => { expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "allow_reading_invalid_state": true, + "auto_calculate_default_ech_capacity": false, "claim_strategy": "update_by_query", "discovery": Object { "active_nodes_lookback": "30s", @@ -75,6 +76,7 @@ describe('config validation', () => { expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "allow_reading_invalid_state": true, + "auto_calculate_default_ech_capacity": false, "claim_strategy": "update_by_query", "discovery": Object { "active_nodes_lookback": "30s", @@ -135,6 +137,7 @@ describe('config validation', () => { expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "allow_reading_invalid_state": true, + "auto_calculate_default_ech_capacity": false, "claim_strategy": "update_by_query", "discovery": Object { "active_nodes_lookback": "30s", diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index db07494ef4f063..f640ed2165f220 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -204,6 +204,7 @@ export const configSchema = schema.object( }), claim_strategy: schema.string({ defaultValue: CLAIM_STRATEGY_UPDATE_BY_QUERY }), request_timeouts: requestTimeoutsConfig, + auto_calculate_default_ech_capacity: schema.boolean({ defaultValue: false }), }, { validate: (config) => { diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 31c873554ee77c..ec459591577707 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -88,6 +88,7 @@ describe('EphemeralTaskLifecycle', () => { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, ...config, }, elasticsearchAndSOAvailability$, diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index 92d97eea7c6b20..ab1d1bc0498fde 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -87,6 +87,7 @@ describe('managed configuration', () => { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }); logger = context.logger.get('taskManager'); @@ -209,6 +210,7 @@ describe('managed configuration', () => { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }); logger = context.logger.get('taskManager'); @@ -334,6 +336,7 @@ describe('managed configuration', () => { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }); logger = context.logger.get('taskManager'); diff --git a/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts b/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts index 24e2f510f949c7..b973a5c1cd5e69 100644 --- a/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts +++ b/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts @@ -60,6 +60,7 @@ const config = { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }; const getStatsWithTimestamp = ({ diff --git a/x-pack/plugins/task_manager/server/lib/get_default_capacity.test.ts b/x-pack/plugins/task_manager/server/lib/get_default_capacity.test.ts index 09a47195082309..76271e6cebeaf9 100644 --- a/x-pack/plugins/task_manager/server/lib/get_default_capacity.test.ts +++ b/x-pack/plugins/task_manager/server/lib/get_default_capacity.test.ts @@ -9,9 +9,56 @@ import { CLAIM_STRATEGY_UPDATE_BY_QUERY, CLAIM_STRATEGY_MGET, DEFAULT_CAPACITY } import { getDefaultCapacity } from './get_default_capacity'; describe('getDefaultCapacity', () => { + it('returns default capacity when autoCalculateDefaultEchCapacity=false', () => { + expect( + getDefaultCapacity({ + autoCalculateDefaultEchCapacity: false, + heapSizeLimit: 851443712, + isCloud: false, + isServerless: false, + isBackgroundTaskNodeOnly: false, + claimStrategy: CLAIM_STRATEGY_MGET, + }) + ).toBe(DEFAULT_CAPACITY); + + expect( + getDefaultCapacity({ + autoCalculateDefaultEchCapacity: false, + heapSizeLimit: 851443712, + isCloud: false, + isServerless: true, + isBackgroundTaskNodeOnly: false, + claimStrategy: CLAIM_STRATEGY_MGET, + }) + ).toBe(DEFAULT_CAPACITY); + + expect( + getDefaultCapacity({ + autoCalculateDefaultEchCapacity: false, + heapSizeLimit: 851443712, + isCloud: false, + isServerless: false, + isBackgroundTaskNodeOnly: true, + claimStrategy: CLAIM_STRATEGY_MGET, + }) + ).toBe(DEFAULT_CAPACITY); + + expect( + getDefaultCapacity({ + autoCalculateDefaultEchCapacity: false, + heapSizeLimit: 851443712, + isCloud: false, + isServerless: true, + isBackgroundTaskNodeOnly: true, + claimStrategy: CLAIM_STRATEGY_MGET, + }) + ).toBe(DEFAULT_CAPACITY); + }); + it('returns default capacity when not in cloud', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: false, isServerless: false, @@ -22,6 +69,7 @@ describe('getDefaultCapacity', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: false, isServerless: true, @@ -32,6 +80,7 @@ describe('getDefaultCapacity', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: false, isServerless: false, @@ -42,6 +91,7 @@ describe('getDefaultCapacity', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: false, isServerless: true, @@ -54,6 +104,7 @@ describe('getDefaultCapacity', () => { it('returns default capacity when default claim strategy', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: true, isServerless: false, @@ -64,6 +115,7 @@ describe('getDefaultCapacity', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: true, isServerless: false, @@ -76,6 +128,7 @@ describe('getDefaultCapacity', () => { it('returns default capacity when serverless', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: false, isServerless: true, @@ -86,6 +139,7 @@ describe('getDefaultCapacity', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: false, isServerless: true, @@ -96,6 +150,7 @@ describe('getDefaultCapacity', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: true, isServerless: true, @@ -106,6 +161,7 @@ describe('getDefaultCapacity', () => { expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: true, isServerless: true, @@ -119,6 +175,7 @@ describe('getDefaultCapacity', () => { // 1GB expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: true, isServerless: false, @@ -130,6 +187,7 @@ describe('getDefaultCapacity', () => { // 1GB but somehow background task node only is true expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 851443712, isCloud: true, isServerless: false, @@ -141,6 +199,7 @@ describe('getDefaultCapacity', () => { // 2GB expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 1702887424, isCloud: true, isServerless: false, @@ -152,6 +211,7 @@ describe('getDefaultCapacity', () => { // 2GB but somehow background task node only is true expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 1702887424, isCloud: true, isServerless: false, @@ -163,6 +223,7 @@ describe('getDefaultCapacity', () => { // 4GB expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 3405774848, isCloud: true, isServerless: false, @@ -174,6 +235,7 @@ describe('getDefaultCapacity', () => { // 4GB background task only expect( getDefaultCapacity({ + autoCalculateDefaultEchCapacity: true, heapSizeLimit: 3405774848, isCloud: true, isServerless: false, diff --git a/x-pack/plugins/task_manager/server/lib/get_default_capacity.ts b/x-pack/plugins/task_manager/server/lib/get_default_capacity.ts index dff31ae3afd501..113747f2196a86 100644 --- a/x-pack/plugins/task_manager/server/lib/get_default_capacity.ts +++ b/x-pack/plugins/task_manager/server/lib/get_default_capacity.ts @@ -8,6 +8,7 @@ import { CLAIM_STRATEGY_MGET, DEFAULT_CAPACITY } from '../config'; interface GetDefaultCapacityOpts { + autoCalculateDefaultEchCapacity: boolean; claimStrategy?: string; heapSizeLimit: number; isCloud: boolean; @@ -24,6 +25,7 @@ const HEAP_TO_CAPACITY_MAP = [ ]; export function getDefaultCapacity({ + autoCalculateDefaultEchCapacity, claimStrategy, heapSizeLimit: heapSizeLimitInBytes, isCloud, @@ -31,7 +33,12 @@ export function getDefaultCapacity({ isBackgroundTaskNodeOnly, }: GetDefaultCapacityOpts) { // perform heap size based calculations only in cloud - if (isCloud && !isServerless && claimStrategy === CLAIM_STRATEGY_MGET) { + if ( + autoCalculateDefaultEchCapacity && + isCloud && + !isServerless && + claimStrategy === CLAIM_STRATEGY_MGET + ) { // convert bytes to GB const heapSizeLimitInGB = heapSizeLimitInBytes / 1e9; diff --git a/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts b/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts index 6b768a9f4d4e97..e56d57e1705581 100644 --- a/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts +++ b/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts @@ -78,6 +78,7 @@ const config: TaskManagerConfig = { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }; describe('createAggregator', () => { diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 2be1930786fa8d..1bcd3e286d4a3f 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -56,6 +56,7 @@ describe('Configuration Statistics Aggregator', () => { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }; const managedConfig = { diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index a7d452f76d6e2a..890c7daf7a111b 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -87,6 +87,7 @@ const pluginInitializerContextParams = { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }; describe('TaskManagerPlugin', () => { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 87acf096d007cf..56f73ed1cc6c39 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -286,6 +286,7 @@ export class TaskManagerPlugin const isServerless = this.initContext.env.packageInfo.buildFlavor === 'serverless'; const defaultCapacity = getDefaultCapacity({ + autoCalculateDefaultEchCapacity: this.config.auto_calculate_default_ech_capacity, claimStrategy: this.config?.claim_strategy, heapSizeLimit: this.heapSizeLimit, isCloud: cloud?.isCloudEnabled ?? false, @@ -300,7 +301,9 @@ export class TaskManagerPlugin this.config!.claim_strategy } isBackgroundTaskNodeOnly=${this.isNodeBackgroundTasksOnly()} heapSizeLimit=${ this.heapSizeLimit - } defaultCapacity=${defaultCapacity}` + } defaultCapacity=${defaultCapacity} autoCalculateDefaultEchCapacity=${ + this.config.auto_calculate_default_ech_capacity + }` ); const managedConfiguration = createManagedConfiguration({ diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 0b6d4ce983d5bf..ce874833b5c38c 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -91,6 +91,7 @@ describe('TaskPollingLifecycle', () => { request_timeouts: { update_by_query: 1000, }, + auto_calculate_default_ech_capacity: false, }, taskStore: mockTaskStore, logger: taskManagerLogger, From aec74fc6e700416d2944ea336290289959e55d83 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 18:49:21 +0100 Subject: [PATCH 35/50] skip flaky suite (#172549) --- .../e2e/response_actions/response_actions_history.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_actions_history.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_actions_history.cy.ts index dfd67d6854b631..93c6f0698a81a5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_actions_history.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_actions_history.cy.ts @@ -10,7 +10,8 @@ import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; import { login } from '../../tasks/login'; import { loadPage } from '../../tasks/common'; -describe( +// FLAKY: https://github.com/elastic/kibana/issues/172549 +describe.skip( 'Response actions history page', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { From 74470ecd62d98401fcdfead3a4113f0729caa9c0 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 18:50:12 +0100 Subject: [PATCH 36/50] skip flaky suite (#195477) --- .../e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts index 424b3fc954c57d..2a3d2876ea488b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts @@ -23,7 +23,8 @@ import { setSecuritySolutionEndpointGroupPrivilege, } from '../../screens/stack_management/role_page'; -describe( +// FLAKY: https://github.com/elastic/kibana/issues/195477 +describe.skip( 'When defining a kibana role for Endpoint security access with space awareness enabled', { // TODO:PR Remove `'@skipInServerlessMKI` once PR merges to `main` From 550015b0410963173cbfed4dd994288978ec9e30 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 18:51:30 +0100 Subject: [PATCH 37/50] skip flaky suite (#192222) --- .../knowledge_base/knowledge_base_user_instructions.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts index bf2eef14db553d..04e05fc9ad31b3 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -51,7 +51,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { await clearConversations(es); }); - describe('when creating private and public user instructions', () => { + // FLAKY: https://github.com/elastic/kibana/issues/192222 + describe.skip('when creating private and public user instructions', () => { before(async () => { await clearKnowledgeBase(es); From bb6e78ebbf8304c7be34345ed215674dc509632a Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:13:04 +0100 Subject: [PATCH 38/50] [Observability] Unified Kubernetes Observability with OpenTelemetry (#194169) --- .../observability_onboarding_flow.tsx | 4 + .../public/application/pages/index.ts | 1 + .../application/pages/otel_kubernetes.tsx | 36 ++ .../kubernetes/use_kubernetes_flow.ts | 18 +- .../otel_kubernetes/index.tsx | 8 + .../otel_kubernetes/otel_kubernetes_panel.tsx | 331 ++++++++++++++++++ .../quickstart_flows/shared/empty_prompt.tsx | 2 +- .../shared/get_started_panel.tsx | 55 +-- .../server/routes/kubernetes/route.ts | 12 +- 9 files changed, 434 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/otel_kubernetes.tsx create mode 100644 x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/index.tsx create mode 100644 x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/observability_onboarding_flow.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/observability_onboarding_flow.tsx index 95b31aab249661..348b3c65f93719 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/observability_onboarding_flow.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/observability_onboarding_flow.tsx @@ -16,6 +16,7 @@ import { KubernetesPage, LandingPage, OtelLogsPage, + OtelKubernetesPage, SystemLogsPage, FirehosePage, } from './pages'; @@ -50,6 +51,9 @@ export function ObservabilityOnboardingFlow() { + + + diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/index.ts b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/index.ts index cd4b821f3cc0dc..7e5606205b6072 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/index.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/index.ts @@ -8,6 +8,7 @@ export { AutoDetectPage } from './auto_detect'; export { CustomLogsPage } from './custom_logs'; export { KubernetesPage } from './kubernetes'; +export { OtelKubernetesPage } from './otel_kubernetes'; export { LandingPage } from './landing'; export { OtelLogsPage } from './otel_logs'; export { SystemLogsPage } from './system_logs'; diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/otel_kubernetes.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/otel_kubernetes.tsx new file mode 100644 index 00000000000000..c4fba1fd8ff0e4 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/pages/otel_kubernetes.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { OtelKubernetesPanel } from '../quickstart_flows/otel_kubernetes/otel_kubernetes_panel'; +import { PageTemplate } from './template'; +import { CustomHeader } from '../header'; + +export const OtelKubernetesPage = () => ( + + } + > + + +); diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/use_kubernetes_flow.ts b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/use_kubernetes_flow.ts index e0a8c7722290fa..4e8a54ccd77e7a 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/use_kubernetes_flow.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/use_kubernetes_flow.ts @@ -11,7 +11,9 @@ import { OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT } from '../../.. import { ObservabilityOnboardingAppServices } from '../../..'; import { useFetcher } from '../../../hooks/use_fetcher'; -export function useKubernetesFlow() { +export function useKubernetesFlow( + onboardingFlowType: 'kubernetes_otel' | 'kubernetes' = 'kubernetes' +) { const { services: { analytics, @@ -20,21 +22,27 @@ export function useKubernetesFlow() { } = useKibana(); const { data, status, error, refetch } = useFetcher( (callApi) => { - return callApi('POST /internal/observability_onboarding/kubernetes/flow'); + return callApi('POST /internal/observability_onboarding/kubernetes/flow', { + params: { + body: { + pkgName: onboardingFlowType, + }, + }, + }); }, - [], + [onboardingFlowType], { showToastOnError: false } ); useEffect(() => { if (data?.onboardingId !== undefined) { analytics?.reportEvent(OBSERVABILITY_ONBOARDING_FLOW_PROGRESS_TELEMETRY_EVENT.eventType, { - onboardingFlowType: 'kubernetes', + onboardingFlowType, onboardingId: data?.onboardingId, step: 'in_progress', }); } - }, [analytics, cloudServiceProvider, data?.onboardingId]); + }, [onboardingFlowType, analytics, cloudServiceProvider, data?.onboardingId]); return { data, status, error, refetch } as const; } diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/index.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/index.tsx new file mode 100644 index 00000000000000..596a035d4b4d13 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/index.tsx @@ -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 { OtelKubernetesPanel } from './otel_kubernetes_panel'; diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx new file mode 100644 index 00000000000000..c745793c47b3ac --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx @@ -0,0 +1,331 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiPanel, + EuiSkeletonText, + EuiSpacer, + EuiSteps, + EuiButtonGroup, + EuiIconTip, + EuiCodeBlock, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; +import { EmptyPrompt } from '../shared/empty_prompt'; +import { GetStartedPanel } from '../shared/get_started_panel'; +import { FeedbackButtons } from '../shared/feedback_buttons'; +import { CopyToClipboardButton } from '../shared/copy_to_clipboard_button'; +import { ObservabilityOnboardingContextValue } from '../../../plugin'; +import { useKubernetesFlow } from '../kubernetes/use_kubernetes_flow'; + +const CLUSTER_OVERVIEW_DASHBOARD_ID = 'kubernetes_otel-cluster-overview'; + +export const OtelKubernetesPanel: React.FC = () => { + const { data, error, refetch } = useKubernetesFlow('kubernetes_otel'); + const [idSelected, setIdSelected] = useState('nodejs'); + const { + services: { share }, + } = useKibana(); + const apmLocator = share.url.locators.get('APM_LOCATOR'); + const dashboardLocator = share.url.locators.get(DASHBOARD_APP_LOCATOR); + + if (error) { + return ( + + ); + } + + const namespace = 'opentelemetry-operator-system'; + const valuesFile = + 'https://raw.githubusercontent.com/elastic/opentelemetry/refs/heads/main/resources/kubernetes/operator/helm/values.yaml'; + + const addRepoCommand = `helm repo add open-telemetry 'https://open-telemetry.github.io/opentelemetry-helm-charts' --force-update`; + const installStackCommand = data + ? `kubectl create namespace ${namespace} +kubectl create secret generic elastic-secret-otel \\ + --namespace ${namespace} \\ + --from-literal=elastic_endpoint='${data.elasticsearchUrl}' \\ + --from-literal=elastic_api_key='${data.apiKeyEncoded}' +helm install opentelemetry-kube-stack open-telemetry/opentelemetry-kube-stack \\ + --namespace ${namespace} \\ + --create-namespace \\ + --values '${valuesFile}'` + : undefined; + + return ( + + + + {addRepoCommand} + + + + + ), + }, + { + title: i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.installStackStepTitle', + { + defaultMessage: 'Install the OpenTelemetry Operator', + } + ), + children: installStackCommand ? ( + <> +

+ + {i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.certmanagerLinkLabel', + { defaultMessage: 'cert-manager' } + )} + + ), + }} + />{' '} + +

+ + + {installStackCommand} + + + + + + + + + {i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.downloadValuesFileButtonEmptyLabel', + { defaultMessage: 'Download values file' } + )} + + + + + ) : ( + + ), + }, + { + title: i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.instrumentApplicationStepTitle', + { + defaultMessage: 'Instrument your application (optional)', + } + ), + children: ( + <> +

+ {i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.theOperatorAutomatesTheLabel', + { + defaultMessage: + 'The Operator automates the injection of auto-instrumentation libraries into the annotated pods for some languages.', + } + )} +

+ + setIdSelected(optionId)} + options={[ + { + id: 'nodejs', + label: 'Node.js', + }, + { + id: 'java', + label: 'Java', + }, + { + id: 'python', + label: 'Python', + }, + { + id: 'dotnet', + label: '.NET', + }, + { + id: 'go', + label: 'Go', + }, + ]} + /> + + + {`apiVersion: v1 +kind: Pod +metadata: + name: my-app + annotations: + instrumentation.opentelemetry.io/inject-${idSelected}: "true" +spec: + containers: + - name: my-app + image: my-app:latest`} + + + + +

+ + {i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.referToTheDocumentationLinkLabel', + { defaultMessage: 'refer to the documentation' } + )} + + ), + }} + /> +

+ + ), + }, + { + title: i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.monitorStepTitle', + { + defaultMessage: 'Visualize your data', + } + ), + children: data ? ( + <> +

+ {i18n.translate( + 'xpack.observability_onboarding.otelKubernetesPanel.onceYourKubernetesInfrastructureLabel', + { + defaultMessage: + 'Analyse your Kubernetes cluster’s health and monitor your container workloads.', + } + )} +

+ + + + ) : ( + + ), + }, + ]} + /> + +
+ ); +}; diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/empty_prompt.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/empty_prompt.tsx index b5b427a718d67d..771948f062fcf6 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/empty_prompt.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/empty_prompt.tsx @@ -35,7 +35,7 @@ export const EmptyPrompt: FunctionComponent = ({ useEffect(() => { analytics?.reportEvent(OBSERVABILITY_ONBOARDING_FLOW_ERROR_TELEMETRY_EVENT.eventType, { onboardingFlowType, - error, + error: error.body?.message ?? error.message, context: telemetryEventContext, }); }, [analytics, error, onboardingFlowType, telemetryEventContext]); diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/get_started_panel.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/get_started_panel.tsx index 98fb6c74a37dee..e529a0782e3955 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/get_started_panel.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/shared/get_started_panel.tsx @@ -37,7 +37,7 @@ export function GetStartedPanel({ }: { onboardingFlowType: string; dataset: string; - integration: string; + integration?: string; newTab: boolean; actionLinks: Array<{ id: string; @@ -88,7 +88,7 @@ export function GetStartedPanel({
- + {actionLinks.map(({ id, title, label, href }) => ( @@ -110,30 +110,33 @@ export function GetStartedPanel({ - - - - - {i18n.translate( - 'xpack.observability_onboarding.dataIngestStatus.viewAllAssetsLinkText', - { - defaultMessage: 'View all assets', - } - )} -
- ), - }} - /> - + {integration && ( + <> + + + + {i18n.translate( + 'xpack.observability_onboarding.dataIngestStatus.viewAllAssetsLinkText', + { + defaultMessage: 'View all assets', + } + )} +
+ ), + }} + /> + + + )} ); } diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts index 7f63ec61249f5e..33a501bd184b91 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts @@ -29,10 +29,14 @@ export interface HasKubernetesDataRouteResponse { const createKubernetesOnboardingFlowRoute = createObservabilityOnboardingServerRoute({ endpoint: 'POST /internal/observability_onboarding/kubernetes/flow', + params: t.type({ + body: t.type({ pkgName: t.union([t.literal('kubernetes'), t.literal('kubernetes_otel')]) }), + }), options: { tags: [] }, async handler({ context, request, + params, plugins, services, kibanaVersion, @@ -55,8 +59,14 @@ const createKubernetesOnboardingFlowRoute = createObservabilityOnboardingServerR const [{ encoded: apiKeyEncoded }, elasticAgentVersion] = await Promise.all([ createShipperApiKey(client.asCurrentUser, 'kubernetes_onboarding'), getAgentVersion(fleetPluginStart, kibanaVersion), - packageClient.ensureInstalledPackage({ pkgName: 'kubernetes' }), + // System package is always required packageClient.ensureInstalledPackage({ pkgName: 'system' }), + // Kubernetes package is required for both classic kubernetes and otel + packageClient.ensureInstalledPackage({ pkgName: 'kubernetes' }), + // Kubernetes otel package is required only for otel + params.body.pkgName === 'kubernetes_otel' + ? packageClient.ensureInstalledPackage({ pkgName: 'kubernetes_otel' }) + : undefined, ]); const elasticsearchUrlList = plugins.cloud?.setup?.elasticsearchUrl From 2d6874981ec036dd992c2481a13228aed5bd68a2 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 8 Oct 2024 14:25:23 -0400 Subject: [PATCH 39/50] Fixing flaky backfill tests (#194328) Resolves https://github.com/elastic/kibana/issues/192144 and https://github.com/elastic/kibana/issues/181862 ## Summary Unskipped the test that was skipped in https://github.com/elastic/kibana/issues/181862 but it did not fail at all during the 2400 times total I ran the flaky test runner so I'm going to unskip and see if it comes back ever While running the flaky test runner, I saw this test from https://github.com/elastic/kibana/issues/192144 be flaky so I addressed in this PR. This was caused by the query for the alert documents sometimes not returning the alert docs in timestamp order, which is important because I'm trying to compare the actual alert timestamp to the expected alert timestamp (in order). Added a sort order to the alert query. --------- Co-authored-by: Elastic Machine --- .../group1/tests/alerting/backfill/find.ts | 3 +- .../tests/alerting/backfill/task_runner.ts | 31 ++++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/find.ts index 829c6770b67aeb..91f10338ac0071 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/find.ts @@ -135,8 +135,7 @@ export default function findBackfillTests({ getService }: FtrProviderContext) { for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; - // FLAKY: https://github.com/elastic/kibana/issues/181862 - describe.skip(scenario.id, () => { + describe(scenario.id, () => { const apiOptions = { spaceId: space.id, username: user.username, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts index e66121a4c33db3..e9fa1724f4bca5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/backfill/task_runner.ts @@ -53,6 +53,7 @@ import { export default function createBackfillTaskRunnerTests({ getService }: FtrProviderContext) { const es = getService('es'); const retry = getService('retry'); + const log = getService('log'); const esTestIndexTool = new ESTestIndexTool(es, retry); const supertestWithoutAuth = getService('supertestWithoutAuth'); const supertest = getService('supertest'); @@ -65,25 +66,25 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide moment().utc().subtract(14, 'days').toISOString(), // backfill execution set 1 - moment().utc().startOf('day').subtract(13, 'days').add(64, 'seconds').toISOString(), - moment().utc().startOf('day').subtract(13, 'days').add(65, 'seconds').toISOString(), - moment().utc().startOf('day').subtract(13, 'days').add(66, 'seconds').toISOString(), + moment().utc().startOf('day').subtract(13, 'days').add(10, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(13, 'days').add(11, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(13, 'days').add(12, 'minutes').toISOString(), // backfill execution set 2 - moment().utc().startOf('day').subtract(12, 'days').add(89, 'seconds').toISOString(), + moment().utc().startOf('day').subtract(12, 'days').add(20, 'minutes').toISOString(), // backfill execution set 3 - moment().utc().startOf('day').subtract(11, 'days').add(785, 'seconds').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(888, 'seconds').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(954, 'seconds').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(1045, 'seconds').toISOString(), - moment().utc().startOf('day').subtract(11, 'days').add(1145, 'seconds').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(30, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(31, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(32, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(33, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(11, 'days').add(34, 'minutes').toISOString(), // backfill execution set 4 purposely left empty // after last backfill - moment().utc().startOf('day').subtract(9, 'days').add(666, 'seconds').toISOString(), - moment().utc().startOf('day').subtract(9, 'days').add(667, 'seconds').toISOString(), + moment().utc().startOf('day').subtract(9, 'days').add(40, 'minutes').toISOString(), + moment().utc().startOf('day').subtract(9, 'days').add(41, 'minutes').toISOString(), ]; describe('ad hoc backfill task', () => { @@ -175,6 +176,9 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide .send([{ rule_id: ruleId, start, end }]) .expect(200); + log.info(`originalDocTimestamps ${JSON.stringify(originalDocTimestamps)}`); + log.info(`scheduledBackfill ${JSON.stringify(response2.body)}`); + const scheduleResult = response2.body; expect(scheduleResult.length).to.eql(1); @@ -668,7 +672,10 @@ export default function createBackfillTaskRunnerTests({ getService }: FtrProvide async function queryForAlertDocs(): Promise>> { const searchResult = await es.search({ index: alertsAsDataIndex, - body: { query: { match_all: {} } }, + body: { + sort: [{ [ALERT_ORIGINAL_TIME]: { order: 'asc' } }], + query: { match_all: {} }, + }, }); return searchResult.hits.hits as Array>; } From 407137a6befb38f34cb11f6a3b6a741a27977031 Mon Sep 17 00:00:00 2001 From: Paulo Silva Date: Tue, 8 Oct 2024 11:56:08 -0700 Subject: [PATCH 40/50] [Fleet] add escapeMultilineString Handlebar helper (#195159) ## Summary Adding a handlebar helper to escape multiline strings. It has the same function as `escapeStringHelper`, but does not wrap strings in single quotes, allowing concatenation of escaped variables in the `hbs` template such as this example: ```hbs audit_rules: "{{escape_multiline_string audit_rules}} {{escape_multiline_string " # Session data audit rules -a always,exit -F arch=b64 -S execve,execveat -k exec -a always,exit -F arch=b64 -S exit_group -a always,exit -F arch=b64 -S setsid"}}" {{else}} {{#if audit_rules}} audit_rules: {{escape_string audit_rules}} {{/if}} {{/if}} ``` The above would not be possible using only `escape_string` as `audit_rules` would be wrapped in single quotes. ## Screenshots The example above illustrates how this option allows the Auditd manager integration to append Session data audit rules to the `audit_rules` field when Session data is enabled in the integration ([PR](https://github.com/elastic/integrations/pull/11336)). image image --- .../server/services/epm/agent/agent.test.ts | 74 +++++++++++++++++++ .../fleet/server/services/epm/agent/agent.ts | 12 +++ 2 files changed, 86 insertions(+) diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index a3e5749384c0bb..7a2a175c4697f1 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -261,6 +261,80 @@ New lines and \\n escaped values.`, }); }); + describe('escape_multiline_string helper', () => { + it('should escape new lines', () => { + const streamTemplate = ` + input: log + multiline_text: "{{escape_multiline_string multiline_text}}" + `; + + const vars = { + multiline_text: { + type: 'textarea', + value: `This is a text with +New lines and \n escaped values.`, + }, + }; + + const output = compileTemplate(vars, streamTemplate); + expect(output).toEqual({ + input: 'log', + multiline_text: `This is a text with +New lines and +escaped values.`, + }); + }); + + it('should escape single quotes', () => { + const streamTemplate = ` + input: log + multiline_text: "{{escape_multiline_string multiline_text}}" + `; + + const vars = { + multiline_text: { + type: 'textarea', + value: `This is a multiline text with +'escaped values.'`, + }, + }; + + const output = compileTemplate(vars, streamTemplate); + expect(output).toEqual({ + input: 'log', + multiline_text: `This is a multiline text with +''escaped values.''`, + }); + }); + + it('should allow concatenation of multiline strings', () => { + const streamTemplate = ` +input: log +multiline_text: "{{escape_multiline_string multiline_text}}{{escape_multiline_string " +This is a concatenated text +with new lines"}}" + `; + + const vars = { + multiline_text: { + type: 'textarea', + value: `This is a text with +New lines and\nescaped values.`, + }, + }; + + const output = compileTemplate(vars, streamTemplate); + expect(output).toEqual({ + input: 'log', + multiline_text: `This is a text with +New lines and +escaped values. +This is a concatenated text +with new lines`, + }); + }); + }); + describe('to_json helper', () => { const streamTemplate = ` input: log diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index 29d6c3c18fc569..8cfa5305413a59 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -131,6 +131,18 @@ function escapeStringHelper(str: string) { } handlebars.registerHelper('escape_string', escapeStringHelper); +/** + * escapeMultilineStringHelper will escape a multiline string by doubling the newlines + * and escaping single quotes. + * This is useful when the string is multiline and needs to be escaped in a yaml file + * without wrapping it in single quotes. + */ +function escapeMultilineStringHelper(str: string) { + if (!str) return undefined; + return str.replace(/\'/g, "''").replace(/\n/g, '\n\n'); +} +handlebars.registerHelper('escape_multiline_string', escapeMultilineStringHelper); + // toJsonHelper will convert any object to a Json string. function toJsonHelper(value: any) { if (typeof value === 'string') { From 0c5c7124766af7e719bf57fe54497d0084541938 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 8 Oct 2024 21:17:38 +0200 Subject: [PATCH 41/50] [ftr] print response body for API key creation failure (#195379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary We are currently investigating test failures against ESS deployment: Failed to create API key for user `admin` role ``` [00:00:00] └-> "before all" hook in "Burn rate rule" [00:00:00] │ debg new cloud SAML authentication with 'admin' role [00:00:00] │ debg Requesting url (redacted): [https://bk-stateful-ftr-16-2c9ee40b2d03.kb.eu-west-1.aws.qa.cld.elstc.co/api/status] [00:00:00] │ info Reading cloud user credentials from /root/.qaf/data/git/kibana/.ftr/role_users.json [00:00:04] └- ✖ fail: apis Slo - Burn rate rule Burn rate rule "before all" hook in "Burn rate rule" [00:00:04] │ Error: expected 400 to equal 200 [00:00:04] │ at Assertion.assert (expect.js:100:11) [00:00:04] │ at Assertion.apply (expect.js:227:8) [00:00:04] │ at Assertion.be (expect.js:69:22) [00:00:04] │ at Object.createM2mApiKeyWithRoleScope (saml_auth_provider.ts:113:25) [00:00:04] │ at processTicksAndRejections (node:internal/process/task_queues:95:5) [00:00:04] │ at Context. (burn_rate_rule.ts:40:24) [00:00:04] │ at Object.apply (wrap_function.js:74:16) ``` Currently we get only status code and it doesn't tell much about the issue. This PR will print response body + status code --- .../services/saml_auth/saml_auth_provider.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts index 67d77583af7134..1ee239ac5448e8 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts @@ -101,7 +101,7 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { }; } - const { body, status } = await supertestWithoutAuth + const response = await supertestWithoutAuth .post('/internal/security/api_key') .set(INTERNAL_REQUEST_HEADERS) .set(adminCookieHeader) @@ -110,9 +110,14 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { metadata: {}, role_descriptors: roleDescriptors, }); - expect(status).to.be(200); - const apiKey = body; + if (response.status !== 200) { + throw new Error( + `Failed to create API key for '${role}' role with response text: ${response.text}` + ); + } + + const apiKey = response.body; const apiKeyHeader = { Authorization: 'ApiKey ' + apiKey.encoded }; log.debug(`Created api key for role: [${role}]`); From 187afe78ce7b047026b3d9dd6e98a67fe92a3047 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 20:21:09 +0100 Subject: [PATCH 42/50] skip flaky suite (#170370) --- .../response_actions/response_console/process_operations.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts index e09aa8dc9fc859..9484122c013d7f 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts @@ -26,7 +26,8 @@ import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_ const AGENT_BEAT_FILE_PATH_SUFFIX = '/components/agentbeat'; -describe('Response console', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { +// FLAKY: https://github.com/elastic/kibana/issues/170370 +describe.skip('Response console', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { beforeEach(() => { login(); }); From 31f4f2c9842f3f6713e004be22087379e9ccf3e7 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 8 Oct 2024 21:23:12 +0200 Subject: [PATCH 43/50] [deps] Replace compare-versions with semver (#195287) ## Summary We are reducing the number of dependencies by replacing the `compare-versions` library with the already used `semver` library that offer the same functionality. --- package.json | 1 - packages/kbn-sort-predicates/src/sorting.ts | 7 +++++-- .../vis_types/vega/public/data_model/vega_parser.ts | 8 ++++++-- .../public/pages/rules/rules_container.tsx | 7 +++++-- yarn.lock | 5 ----- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 1b4a27b012a602..cd72443e901720 100644 --- a/package.json +++ b/package.json @@ -1075,7 +1075,6 @@ "classnames": "2.2.6", "color": "^4.2.3", "commander": "^4.1.1", - "compare-versions": "3.5.1", "constate": "^3.3.2", "copy-to-clipboard": "^3.0.8", "core-js": "^3.37.1", diff --git a/packages/kbn-sort-predicates/src/sorting.ts b/packages/kbn-sort-predicates/src/sorting.ts index d56ca8f550c801..8adfc237bc7f54 100644 --- a/packages/kbn-sort-predicates/src/sorting.ts +++ b/packages/kbn-sort-predicates/src/sorting.ts @@ -7,8 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import versionCompare from 'compare-versions'; import valid from 'semver/functions/valid'; +import semVerCompare from 'semver/functions/compare'; +import semVerCoerce from 'semver/functions/coerce'; import ipaddr, { type IPv4, type IPv6 } from 'ipaddr.js'; import { FieldFormat } from '@kbn/field-formats-plugin/common'; import moment from 'moment'; @@ -154,7 +155,9 @@ const versionComparison: CompareFn = (v1, v2, direction) => { if (bInvalid) { return direction * -1; } - return versionCompare(valueA, valueB); + const semVerValueA = semVerCoerce(valueA) ?? ''; + const semVerValueB = semVerCoerce(valueB) ?? ''; + return semVerCompare(semVerValueA, semVerValueB); }; const openRange = { gte: -Infinity, lt: Infinity }; diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts index d41ec9759373ca..0a0d0ec4151aec 100644 --- a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts @@ -9,7 +9,8 @@ import _ from 'lodash'; import schemaParser from 'vega-schema-url-parser'; -import versionCompare from 'compare-versions'; +import semVerCompare from 'semver/functions/compare'; +import semVerCoerce from 'semver/functions/coerce'; import hjson from 'hjson'; import { euiPaletteColorBlind } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -17,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { logger, Warn, None, version as vegaVersion } from 'vega'; import { compile, TopLevelSpec, version as vegaLiteVersion } from 'vega-lite'; + import { EsQueryParser } from './es_query_parser'; import { Utils } from './utils'; import { EmsFileParser } from './ems_file_parser'; @@ -558,8 +560,10 @@ The URL is an identifier only. Kibana and your browser will never access this UR const schema = schemaParser(spec.$schema); const isVegaLite = schema.library === 'vega-lite'; const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion; + const schemaSemVer = semVerCoerce(schema.version) ?? ''; + const libSemVersion = semVerCoerce(libVersion) ?? ''; - if (versionCompare(schema.version, libVersion) > 0) { + if (semVerCompare(schemaSemVer, libSemVersion) > 0) { this._onWarning( i18n.translate( 'visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage', diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index 1b1af9aab42923..8eaec34adf1cfe 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -5,7 +5,6 @@ * 2.0. */ import React, { useState, useMemo, useEffect } from 'react'; -import compareVersions from 'compare-versions'; import { EuiSpacer } from '@elastic/eui'; import { useParams, useHistory, generatePath } from 'react-router-dom'; import type { @@ -14,6 +13,8 @@ import type { RuleStateAttributes, } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import { extractErrorMessage } from '@kbn/cloud-security-posture-common'; +import semVerCompare from 'semver/functions/compare'; +import semVerCoerce from 'semver/functions/coerce'; import { benchmarksNavigation } from '../../common/navigation/constants'; import { buildRuleKey } from '../../../common/utils/rules_states'; import { RulesTable } from './rules_table'; @@ -197,7 +198,9 @@ export const RulesContainer = () => { return a.localeCompare(b, 'en', { sensitivity: 'base' }); }); - const cleanedRuleNumberList = [...new Set(ruleNumberList)].sort(compareVersions); + const cleanedRuleNumberList = [...new Set(ruleNumberList)].sort((a, b) => + semVerCompare(semVerCoerce(a) ?? '', semVerCoerce(b) ?? '') + ); const rulesPageData = useMemo( () => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery), diff --git a/yarn.lock b/yarn.lock index 465601b23df341..1c831df8f53e8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14706,11 +14706,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -compare-versions@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" - integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== - compare-versions@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" From 4ad15861489904aae1b8a0bb3d35bf3fca6eb729 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 8 Oct 2024 20:23:33 +0100 Subject: [PATCH 44/50] [Ownership] Fixup wrong assignment (#195462) ## Summary Fixup line broken by this [merge](https://github.com/elastic/kibana/pull/195272). Contributes to: https://github.com/elastic/kibana/issues/194815 --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d4c8f52704e967..a20e12d88c3525 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1013,6 +1013,8 @@ packages/kbn-zod-helpers @elastic/security-detection-rule-management # The #CC# prefix delineates Code Coverage, # used for the 'team' designator within Kibana Stats +x-pack/test_serverless/api_integration/test_suites/common/platform_security @elastic/kibana-security + # Data Discovery /x-pack/test_serverless/functional/es_archives/pre_calculated_histogram @elastic/kibana-data-discovery /x-pack/test_serverless/functional/es_archives/kibana_sample_data_flights_index_pattern @elastic/kibana-data-discovery @@ -1511,7 +1513,6 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints /x-pack/test/functional/es_archives/auditbeat/default @elastic/security-solution /x-pack/test/functional/es_archives/auditbeat/hosts @elastic/security-solution /x-pack/test_serverless/functional/page_objects/svl_management_page.ts @elastic/security-solution -/x-pack/test_serverless/api_integration/test_suites/common/platform_security/ @elastic/security-solution /x-pack/test_serverless/api_integration/test_suites/security @elastic/security-solution /x-pack/test_serverless/functional/page_objects/svl_sec_landing_page.ts @elastic/security-solution From e98fa183aaa050999b524425b7dd1479d64c9426 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 20:27:10 +0100 Subject: [PATCH 45/50] skip flaky suite (#193294) --- .../test_suites/observability/onboarding/firehose.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts b/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts index 22c700d16be2df..c2d2161c7491a8 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts @@ -20,7 +20,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const synthtrace = getService('svlLogsSynthtraceClient'); - describe('Onboarding Firehose Quickstart Flow', () => { + // FLAKY: https://github.com/elastic/kibana/issues/193294 + describe.skip('Onboarding Firehose Quickstart Flow', () => { before(async () => { await PageObjects.svlCommonPage.loginAsAdmin(); // Onboarding requires admin role await PageObjects.common.navigateToUrlWithBrowserHistory( From 13c949462931960a4ca5eb82f9dbe955893d426b Mon Sep 17 00:00:00 2001 From: "elastic-renovate-prod[bot]" <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:44:39 +0200 Subject: [PATCH 46/50] Update dependency re2js to v0.4.2 (main) (#193897) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index cd72443e901720..02b7f61924abbc 100644 --- a/package.json +++ b/package.json @@ -1199,7 +1199,7 @@ "query-string": "^6.13.2", "rbush": "^3.0.1", "re-resizable": "^6.9.9", - "re2js": "0.4.1", + "re2js": "0.4.2", "react": "^17.0.2", "react-ace": "^7.0.5", "react-diff-view": "^3.2.1", diff --git a/yarn.lock b/yarn.lock index 1c831df8f53e8b..c3ec4698df7d2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26595,10 +26595,10 @@ re-resizable@^6.9.9: resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216" integrity sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA== -re2js@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/re2js/-/re2js-0.4.1.tgz#74a87a90b79ab5dc1effed818151354c8faccb3d" - integrity sha512-Kxb+OKXrEPowP4bXAF07NDXtgYX07S8HeVGgadx5/D/R41LzWg1kgTD2szIv2iHJM3vrAPnDKaBzfUE/7QWX9w== +re2js@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/re2js/-/re2js-0.4.2.tgz#e344697e64d128ea65c121d6581e67ee5bfa5feb" + integrity sha512-wuv0p0BGbrVIkobV8zh82WjDurXko0QNCgaif6DdRAljgVm2iio4PVYCwjAxGaWen1/QZXWDM67dIslmz7AIbA== react-ace@^7.0.5: version "7.0.5" From 942a1f1e693c5ff86cbcbd93d65bde0915fed274 Mon Sep 17 00:00:00 2001 From: Mario Duarte Date: Tue, 8 Oct 2024 21:57:54 +0200 Subject: [PATCH 47/50] CP-7782 - Replace E2E pipeline execution with serverless-quality-gates CHECK_SYNTHETICS (#195214) --- .../emergency/pipeline.tests-production.yaml | 12 +++++++----- .../quality-gates/emergency/pipeline.tests-qa.yaml | 12 +++++++----- .../emergency/pipeline.tests-staging.yaml | 12 +++++++----- .../quality-gates/pipeline.tests-production.yaml | 12 +++++++----- .../pipelines/quality-gates/pipeline.tests-qa.yaml | 12 +++++++----- .../quality-gates/pipeline.tests-staging.yaml | 12 +++++++----- 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-production.yaml b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-production.yaml index f7689ffeab9285..c81208c7cf61c9 100644 --- a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-production.yaml +++ b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-production.yaml @@ -16,14 +16,16 @@ steps: DEPLOYMENT_SLICES: ${DEPLOYMENT_SLICES:-""} soft_fail: true - - label: ":rocket: control-plane e2e tests" + - label: ":rocket: Run serverless synthetics check" if: build.env("ENVIRONMENT") == "production-canary" - trigger: "ess-k8s-production-e2e-tests" # https://buildkite.com/elastic/ess-k8s-production-e2e-tests + trigger: "serverless-quality-gates" build: - env: - REGION_ID: aws-us-east-1 - NAME_PREFIX: ci_test_kibana-promotion_ message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-production.yaml)" + env: + TARGET_ENV: production + SERVICE: kibana + CHECK_SYNTHETICS: true + CHECK_SYNTHETICS_TAG: serverless-platform-core-validation - label: ":cookie: 24h bake time before continuing promotion" if: build.env("ENVIRONMENT") == "production-canary" diff --git a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-qa.yaml b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-qa.yaml index 1c0e69ef7a7b4e..3c2f123914c3df 100644 --- a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-qa.yaml +++ b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-qa.yaml @@ -3,10 +3,12 @@ # A failure in this pipeline build will prevent further progression to the subsequent stage. steps: - - label: ":rocket: control-plane e2e tests" - trigger: "ess-k8s-qa-e2e-tests-daily" # https://buildkite.com/elastic/ess-k8s-qa-e2e-tests-daily + - label: ":rocket: Run serverless synthetics check" + trigger: "serverless-quality-gates" build: - env: - REGION_ID: aws-eu-west-1 - NAME_PREFIX: ci_test_kibana-promotion_ message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-qa.yaml)" + env: + TARGET_ENV: qa + SERVICE: kibana + CHECK_SYNTHETICS: true + CHECK_SYNTHETICS_TAG: serverless-platform-core-validation \ No newline at end of file diff --git a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml index 2bd85e2ad8a741..5c2da1b4fe891a 100644 --- a/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml +++ b/.buildkite/pipelines/quality-gates/emergency/pipeline.tests-staging.yaml @@ -3,13 +3,15 @@ # A failure in this pipeline build will prevent further progression to the subsequent stage. steps: - - label: ":rocket: control-plane e2e tests" - trigger: "ess-k8s-staging-e2e-tests" # https://buildkite.com/elastic/ess-k8s-staging-e2e-tests + - label: ":rocket: Run serverless synthetics check" + trigger: "serverless-quality-gates" build: - env: - REGION_ID: aws-us-east-1 - NAME_PREFIX: ci_test_kibana-promotion_ message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-staging.yaml)" + env: + TARGET_ENV: staging + SERVICE: kibana + CHECK_SYNTHETICS: true + CHECK_SYNTHETICS_TAG: serverless-platform-core-validation - label: ":kibana: Kibana Serverless Tests for ${ENVIRONMENT}" trigger: appex-qa-serverless-kibana-ftr-tests # https://buildkite.com/elastic/appex-qa-serverless-kibana-ftr-tests diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml index f7689ffeab9285..c81208c7cf61c9 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml @@ -16,14 +16,16 @@ steps: DEPLOYMENT_SLICES: ${DEPLOYMENT_SLICES:-""} soft_fail: true - - label: ":rocket: control-plane e2e tests" + - label: ":rocket: Run serverless synthetics check" if: build.env("ENVIRONMENT") == "production-canary" - trigger: "ess-k8s-production-e2e-tests" # https://buildkite.com/elastic/ess-k8s-production-e2e-tests + trigger: "serverless-quality-gates" build: - env: - REGION_ID: aws-us-east-1 - NAME_PREFIX: ci_test_kibana-promotion_ message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-production.yaml)" + env: + TARGET_ENV: production + SERVICE: kibana + CHECK_SYNTHETICS: true + CHECK_SYNTHETICS_TAG: serverless-platform-core-validation - label: ":cookie: 24h bake time before continuing promotion" if: build.env("ENVIRONMENT") == "production-canary" diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml index 3d9363279507f2..6730ad5c5840da 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml @@ -29,13 +29,15 @@ steps: ENVIRONMENT: ${ENVIRONMENT} message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-qa.yaml)" - - label: ":rocket: control-plane e2e tests" - trigger: "ess-k8s-qa-e2e-tests-daily" # https://buildkite.com/elastic/ess-k8s-qa-e2e-tests-daily + - label: ":rocket: Run serverless synthetics check" + trigger: "serverless-quality-gates" build: - env: - REGION_ID: aws-eu-west-1 - NAME_PREFIX: ci_test_kibana-promotion_ message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-qa.yaml)" + env: + TARGET_ENV: qa + SERVICE: kibana + CHECK_SYNTHETICS: true + CHECK_SYNTHETICS_TAG: serverless-platform-core-validation - wait: ~ diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml index 2bd85e2ad8a741..5c2da1b4fe891a 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml @@ -3,13 +3,15 @@ # A failure in this pipeline build will prevent further progression to the subsequent stage. steps: - - label: ":rocket: control-plane e2e tests" - trigger: "ess-k8s-staging-e2e-tests" # https://buildkite.com/elastic/ess-k8s-staging-e2e-tests + - label: ":rocket: Run serverless synthetics check" + trigger: "serverless-quality-gates" build: - env: - REGION_ID: aws-us-east-1 - NAME_PREFIX: ci_test_kibana-promotion_ message: "${BUILDKITE_MESSAGE} (triggered by pipeline.tests-staging.yaml)" + env: + TARGET_ENV: staging + SERVICE: kibana + CHECK_SYNTHETICS: true + CHECK_SYNTHETICS_TAG: serverless-platform-core-validation - label: ":kibana: Kibana Serverless Tests for ${ENVIRONMENT}" trigger: appex-qa-serverless-kibana-ftr-tests # https://buildkite.com/elastic/appex-qa-serverless-kibana-ftr-tests From 91c045d698b2e68afd13f5d4bef9229d8a231abe Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 8 Oct 2024 14:34:01 -0600 Subject: [PATCH 48/50] [Canvas] Cleanup services (#194634) Closes https://github.com/elastic/kibana/issues/194050 ## Summary This PR refactors the Canvas services to no longer use the `PluginServiceProvider` from the `PresentationUtil` plugin. Note that the Canvas storybooks are broken on main (and they have been for who knows how long) and so, while I did make some changes to the storybooks to make them **compile**, I didn't bother to get them fully functional. Note that the Ecommerce workpad is broken - this is not due to this PR, it is a [bug](https://github.com/elastic/kibana/issues/195297) that is present on main. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Catherine Liu --- .../renderers/embeddable/embeddable.tsx | 5 +- x-pack/plugins/canvas/jest.config.js | 1 + .../services/nav_link.ts => jest_setup.ts} | 7 +- x-pack/plugins/canvas/public/application.tsx | 25 +-- .../canvas/public/components/app/index.tsx | 26 ++- .../components/asset_manager/asset_manager.ts | 6 +- .../datasource/datasource_component.js | 11 +- .../datasource/datasource_preview/index.js | 7 +- .../components/element_content/index.tsx | 9 +- .../embeddable_flyout/flyout.component.tsx | 35 ++- .../components/embeddable_flyout/flyout.tsx | 7 +- .../es_data_view_select.component.tsx | 8 +- .../es_data_view_select.tsx | 7 +- .../components/es_field_select/index.tsx | 7 +- .../es_fields_select/es_fields_select.tsx | 7 +- .../public/components/expression/index.tsx | 7 +- .../public/components/function_form/index.tsx | 10 +- .../components/function_form_list/index.js | 4 +- .../public/components/home/home.stories.tsx | 2 +- .../home/hooks/use_clone_workpad.ts | 7 +- .../home/hooks/use_create_from_template.ts | 7 +- .../home/hooks/use_create_workpad.ts | 7 +- .../home/hooks/use_delete_workpad.ts | 83 ++++---- .../home/hooks/use_find_templates.ts | 6 +- .../components/home/hooks/use_find_workpad.ts | 8 +- .../home/hooks/use_import_workpad.ts | 7 +- .../my_workpads/my_workpads.component.tsx | 2 +- .../home/my_workpads/my_workpads.stories.tsx | 2 +- .../home/my_workpads/my_workpads.tsx | 2 +- .../my_workpads/workpad_table.component.tsx | 2 +- .../my_workpads/workpad_table.stories.tsx | 4 +- .../home/my_workpads/workpad_table.tsx | 11 +- .../workpad_table_tools.component.tsx | 2 +- .../workpad_templates.stories.tsx | 2 +- .../public/components/home_app/home_app.tsx | 7 +- .../hooks/workpad/use_download_workpad.ts | 7 +- .../hooks/workpad/use_incoming_embeddable.ts | 8 +- .../saved_elements_modal.tsx | 7 +- .../public/components/workpad/workpad.tsx | 6 +- .../hooks/use_canvas_filters.ts | 6 +- .../editor_menu/editor_menu.tsx | 23 +- .../labs_control/labs_control.tsx | 10 +- .../flyout/hooks/use_download_runtime.ts | 18 +- .../workpad_header/share_menu/share_menu.tsx | 22 +- .../canvas/public/functions/filters.ts | 6 +- x-pack/plugins/canvas/public/lib/assets.ts | 5 +- .../canvas/public/lib/create_handlers.ts | 7 +- .../canvas/public/lib/data_view_helpers.ts | 26 +++ .../public/lib/element_handler_creators.ts | 11 +- .../plugins/canvas/public/lib/fullscreen.js | 7 +- .../canvas/public/lib/template_service.ts | 5 +- x-pack/plugins/canvas/public/plugin.tsx | 27 ++- .../use_fullscreen_presentation_helper.ts | 12 +- .../routes/workpad/hooks/use_workpad.test.tsx | 18 +- .../routes/workpad/hooks/use_workpad.ts | 22 +- .../hooks/use_workpad_persist.test.tsx | 15 +- .../workpad/hooks/use_workpad_persist.ts | 15 +- .../workpad/workpad_presentation_helper.tsx | 9 +- .../services/canvas_custom_element_service.ts | 62 ++++++ ...sions.ts => canvas_expressions_service.ts} | 61 +++--- .../filters.ts => canvas_filters_service.ts} | 30 ++- .../notify.ts => canvas_notify_service.ts} | 22 +- .../public/services/canvas_workpad_service.ts | 200 ++++++++++++++++++ .../canvas/public/services/custom_element.ts | 21 -- .../canvas/public/services/data_views.ts | 14 -- .../canvas/public/services/embeddables.ts | 22 -- .../canvas/public/services/expressions.ts | 8 - .../plugins/canvas/public/services/filters.ts | 8 - .../plugins/canvas/public/services/index.ts | 56 +---- .../public/services/kibana/custom_element.ts | 44 ---- .../public/services/kibana/data_views.ts | 50 ----- .../public/services/kibana/embeddables.ts | 22 -- .../canvas/public/services/kibana/index.ts | 66 ------ .../canvas/public/services/kibana/labs.ts | 23 -- .../canvas/public/services/kibana/nav_link.ts | 28 --- .../canvas/public/services/kibana/platform.ts | 46 ---- .../public/services/kibana/reporting.ts | 45 ---- .../public/services/kibana/ui_actions.ts | 19 -- .../public/services/kibana/visualizations.ts | 21 -- .../canvas/public/services/kibana/workpad.ts | 170 --------------- .../canvas/public/services/kibana_services.ts | 76 +++++++ x-pack/plugins/canvas/public/services/labs.ts | 14 -- .../public/services/legacy/reporting.ts | 42 ---- .../plugins/canvas/public/services/mocks.ts | 125 +++++++++++ .../plugins/canvas/public/services/notify.ts | 15 -- .../canvas/public/services/platform.ts | 37 ---- .../canvas/public/services/reporting.ts | 13 -- .../canvas/public/services/storybook/index.ts | 60 ------ .../public/services/storybook/notify.ts | 22 -- .../public/services/storybook/workpad.ts | 122 ----------- .../public/services/stubs/custom_element.ts | 21 -- .../public/services/stubs/data_views.ts | 26 --- .../public/services/stubs/embeddables.ts | 20 -- .../public/services/stubs/expressions.ts | 41 ---- .../canvas/public/services/stubs/filters.ts | 23 -- .../canvas/public/services/stubs/index.ts | 61 ------ .../canvas/public/services/stubs/labs.ts | 25 --- .../canvas/public/services/stubs/nav_link.ts | 17 -- .../canvas/public/services/stubs/notify.ts | 21 -- .../canvas/public/services/stubs/platform.ts | 39 ---- .../public/services/stubs/reporting.tsx | 17 -- .../public/services/stubs/ui_actions.ts | 17 -- .../public/services/stubs/visualizations.ts | 19 -- .../canvas/public/services/stubs/workpad.ts | 115 ---------- .../canvas/public/services/ui_actions.ts | 12 -- .../canvas/public/services/visualizations.ts | 14 -- .../plugins/canvas/public/services/workpad.ts | 43 ---- .../canvas/public/state/actions/elements.js | 32 ++- .../canvas/public/state/actions/filters.js | 13 ++ .../canvas/public/state/initial_state.js | 7 +- .../canvas/public/state/reducers/elements.js | 16 +- .../public/state/reducers/embeddable.ts | 7 +- .../public/state/reducers/resolved_args.js | 8 +- .../canvas/public/state/reducers/transient.js | 31 ++- .../canvas/public/state/reducers/workpad.js | 52 ++--- .../__snapshots__/app.test.tsx.snap | 2 +- .../shareable_runtime/components/app.test.tsx | 14 +- .../__snapshots__/settings.test.tsx.snap | 20 +- .../footer/settings/settings.test.tsx | 14 +- .../shareable_runtime/test/selectors.ts | 2 +- x-pack/plugins/canvas/storybook/constants.ts | 31 +++ .../decorators/services_decorator.tsx | 14 +- x-pack/plugins/canvas/tsconfig.json | 7 +- 123 files changed, 989 insertions(+), 2018 deletions(-) rename x-pack/plugins/canvas/{public/services/nav_link.ts => jest_setup.ts} (65%) create mode 100644 x-pack/plugins/canvas/public/lib/data_view_helpers.ts create mode 100644 x-pack/plugins/canvas/public/services/canvas_custom_element_service.ts rename x-pack/plugins/canvas/public/services/{kibana/expressions.ts => canvas_expressions_service.ts} (65%) rename x-pack/plugins/canvas/public/services/{kibana/filters.ts => canvas_filters_service.ts} (51%) rename x-pack/plugins/canvas/public/services/{kibana/notify.ts => canvas_notify_service.ts} (71%) create mode 100644 x-pack/plugins/canvas/public/services/canvas_workpad_service.ts delete mode 100644 x-pack/plugins/canvas/public/services/custom_element.ts delete mode 100644 x-pack/plugins/canvas/public/services/data_views.ts delete mode 100644 x-pack/plugins/canvas/public/services/embeddables.ts delete mode 100644 x-pack/plugins/canvas/public/services/expressions.ts delete mode 100644 x-pack/plugins/canvas/public/services/filters.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/custom_element.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/data_views.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/embeddables.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/index.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/labs.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/nav_link.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/platform.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/reporting.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/ui_actions.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/visualizations.ts delete mode 100644 x-pack/plugins/canvas/public/services/kibana/workpad.ts create mode 100644 x-pack/plugins/canvas/public/services/kibana_services.ts delete mode 100644 x-pack/plugins/canvas/public/services/labs.ts delete mode 100644 x-pack/plugins/canvas/public/services/legacy/reporting.ts create mode 100644 x-pack/plugins/canvas/public/services/mocks.ts delete mode 100644 x-pack/plugins/canvas/public/services/notify.ts delete mode 100644 x-pack/plugins/canvas/public/services/platform.ts delete mode 100644 x-pack/plugins/canvas/public/services/reporting.ts delete mode 100644 x-pack/plugins/canvas/public/services/storybook/index.ts delete mode 100644 x-pack/plugins/canvas/public/services/storybook/notify.ts delete mode 100644 x-pack/plugins/canvas/public/services/storybook/workpad.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/custom_element.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/data_views.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/embeddables.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/expressions.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/filters.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/index.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/labs.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/nav_link.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/notify.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/platform.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/reporting.tsx delete mode 100644 x-pack/plugins/canvas/public/services/stubs/ui_actions.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/visualizations.ts delete mode 100644 x-pack/plugins/canvas/public/services/stubs/workpad.ts delete mode 100644 x-pack/plugins/canvas/public/services/ui_actions.ts delete mode 100644 x-pack/plugins/canvas/public/services/visualizations.ts delete mode 100644 x-pack/plugins/canvas/public/services/workpad.ts create mode 100644 x-pack/plugins/canvas/public/state/actions/filters.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index d088988c243fe4..a21528fb970fb1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -19,7 +19,6 @@ import React, { FC } from 'react'; import ReactDOM from 'react-dom'; import { useSearchApi } from '@kbn/presentation-publishing'; import { omit } from 'lodash'; -import { pluginServices } from '../../../public/services'; import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib'; import { RendererStrings } from '../../../i18n'; import { @@ -32,6 +31,7 @@ import { EmbeddableExpression } from '../../expression_types/embeddable'; import { StartDeps } from '../../plugin'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; import { useGetAppContext } from './use_get_app_context'; +import { embeddableService } from '../../../public/services/kibana_services'; const { embeddable: strings } = RendererStrings; @@ -132,13 +132,12 @@ export const embeddableRendererFactory = ( help: strings.getHelpDescription(), reuseDomNode: true, render: async (domNode, { input, embeddableType, canvasApi }, handlers) => { - const { embeddables } = pluginServices.getServices(); const uniqueId = handlers.getElementId(); const isByValueEnabled = plugins.presentationUtil.labsService.isProjectEnabled( 'labs:canvas:byValueEmbeddable' ); - if (embeddables.reactEmbeddableRegistryHasKey(embeddableType)) { + if (embeddableService.reactEmbeddableRegistryHasKey(embeddableType)) { /** * Prioritize React embeddables */ diff --git a/x-pack/plugins/canvas/jest.config.js b/x-pack/plugins/canvas/jest.config.js index 2bff284e94ad8b..f7a9224795b4a2 100644 --- a/x-pack/plugins/canvas/jest.config.js +++ b/x-pack/plugins/canvas/jest.config.js @@ -17,4 +17,5 @@ module.exports = { collectCoverageFrom: [ '/x-pack/plugins/canvas/{canvas_plugin_src,common,i18n,public,server,shareable_runtime}/**/*.{js,ts,tsx}', ], + setupFiles: ['/x-pack/plugins/canvas/jest_setup.ts'], }; diff --git a/x-pack/plugins/canvas/public/services/nav_link.ts b/x-pack/plugins/canvas/jest_setup.ts similarity index 65% rename from x-pack/plugins/canvas/public/services/nav_link.ts rename to x-pack/plugins/canvas/jest_setup.ts index 02c7ca50219a63..d9c0c26ea5a382 100644 --- a/x-pack/plugins/canvas/public/services/nav_link.ts +++ b/x-pack/plugins/canvas/jest_setup.ts @@ -5,6 +5,7 @@ * 2.0. */ -export interface CanvasNavLinkService { - updatePath: (path: string) => void; -} +import { setStubKibanaServices } from './public/services/mocks'; + +// Start the kibana services with stubs +setStubKibanaServices(); diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 72440e3e6873c3..222b64e4175e92 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -19,7 +19,6 @@ import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from '@kbn/core/ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { PluginServices } from '@kbn/presentation-util-plugin/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; import { App } from './components/app'; @@ -32,12 +31,7 @@ import { init as initStatsReporter } from './lib/ui_metric'; import { CapabilitiesStrings } from '../i18n'; -import { - startLegacyServices, - services, - LegacyServicesProvider, - CanvasPluginServices, -} from './services'; +import { startLegacyServices, services, LegacyServicesProvider } from './services'; import { initFunctions } from './functions'; // @ts-expect-error untyped local import { appUnload } from './state/actions/app'; @@ -56,29 +50,26 @@ export const renderApp = ({ startPlugins, params, canvasStore, - pluginServices, + appUpdater, }: { coreStart: CoreStart; startPlugins: CanvasStartDeps; params: AppMountParameters; canvasStore: Store; - pluginServices: PluginServices; + appUpdater: BehaviorSubject; }) => { const { element } = params; element.classList.add('canvas'); element.classList.add('canvasContainerWrapper'); - const ServicesContextProvider = pluginServices.getContextProvider(); ReactDOM.render( - - - - - - - + + + + + , element diff --git a/x-pack/plugins/canvas/public/components/app/index.tsx b/x-pack/plugins/canvas/public/components/app/index.tsx index e23891ccc9bca6..0ae3fcf95e6bee 100644 --- a/x-pack/plugins/canvas/public/components/app/index.tsx +++ b/x-pack/plugins/canvas/public/components/app/index.tsx @@ -5,14 +5,17 @@ * 2.0. */ -import React, { FC, useEffect } from 'react'; +import { AppUpdater, ScopedHistory } from '@kbn/core/public'; import PropTypes from 'prop-types'; -import { ScopedHistory } from '@kbn/core/public'; -import { useNavLinkService } from '../../services'; +import React, { FC, useEffect } from 'react'; +import { BehaviorSubject } from 'rxjs'; // @ts-expect-error import { shortcutManager } from '../../lib/shortcut_manager'; import { CanvasRouter } from '../../routes'; import { Flyouts } from '../flyouts'; +import { getSessionStorage } from '../../lib/storage'; +import { SESSIONSTORAGE_LASTPATH } from '../../../common/lib'; +import { coreServices } from '../../services/kibana_services'; class ShortcutManagerContextWrapper extends React.Component> { static childContextTypes = { @@ -28,12 +31,21 @@ class ShortcutManagerContextWrapper extends React.Component = ({ history }) => { - const { updatePath } = useNavLinkService(); - +export const App: FC<{ history: ScopedHistory; appUpdater: BehaviorSubject }> = ({ + history, + appUpdater, +}) => { useEffect(() => { return history.listen(({ pathname, search }) => { - updatePath(pathname + search); + const path = pathname + search; + appUpdater.next(() => ({ + defaultPath: path, + })); + + getSessionStorage().set( + `${SESSIONSTORAGE_LASTPATH}:${coreServices.http.basePath.get()}`, + path + ); }); }); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.ts b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.ts index b46c9f07f7caa9..582c5174b663fb 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.ts +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.ts @@ -20,7 +20,7 @@ import { State, AssetType, CanvasWorkpad } from '../../../types'; import { AssetManager as Component } from './asset_manager.component'; import { getFullWorkpadPersisted } from '../../state/selectors/workpad'; -import { pluginServices } from '../../services'; +import { getCanvasWorkpadService } from '../../services/canvas_workpad_service'; export const AssetManager = connect( (state: State) => ({ @@ -31,7 +31,7 @@ export const AssetManager = connect( onAddAsset: (workpad: CanvasWorkpad, type: AssetType['type'], content: AssetType['value']) => { // make the ID here and pass it into the action const asset = createAsset(type, content); - const { notify, workpad: workpadService } = pluginServices.getServices(); + const workpadService = getCanvasWorkpadService(); return workpadService .updateAssets(workpad.id, { ...workpad.assets, [asset.id]: asset }) @@ -40,7 +40,7 @@ export const AssetManager = connect( // then return the id, so the caller knows the id that will be created return asset.id; }) - .catch((error) => notifyError(error, notify.error)); + .catch((error) => notifyError(error)); }, }), (stateProps, dispatchProps, ownProps) => { diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js index 4b64149d2a8f64..0d610155362941 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js @@ -20,7 +20,7 @@ import { import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { pluginServices } from '../../services'; +import { dataViewsService } from '../../services/kibana_services'; import { DatasourceSelector } from './datasource_selector'; import { DatasourcePreview } from './datasource_preview'; @@ -67,12 +67,9 @@ export class DatasourceComponent extends PureComponent { state = { defaultIndex: '' }; componentDidMount() { - pluginServices - .getServices() - .dataViews.getDefaultDataView() - .then((defaultDataView) => { - this.setState({ defaultIndex: defaultDataView.title }); - }); + dataViewsService.getDefaultDataView().then((defaultDataView) => { + this.setState({ defaultIndex: defaultDataView.title }); + }); } componentDidUpdate(prevProps) { diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js index 1ca674bfb6f9d3..f6f535058ea21d 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js @@ -8,18 +8,17 @@ import React, { useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; import { Loading } from '../../loading'; -import { useExpressionsService } from '../../../services'; +import { getCanvasExpressionService } from '../../../services/canvas_expressions_service'; import { DatasourcePreview as Component } from './datasource_preview'; export const DatasourcePreview = (props) => { const [datatable, setDatatable] = useState(); - const expressionsService = useExpressionsService(); useEffect(() => { - expressionsService + getCanvasExpressionService() .interpretAst({ type: 'expression', chain: [props.function] }, {}) .then(setDatatable); - }, [expressionsService, props.function, setDatatable]); + }, [props.function, setDatatable]); if (!datatable) { return ; diff --git a/x-pack/plugins/canvas/public/components/element_content/index.tsx b/x-pack/plugins/canvas/public/components/element_content/index.tsx index bdbfc205d4c810..72ff04cbf20557 100644 --- a/x-pack/plugins/canvas/public/components/element_content/index.tsx +++ b/x-pack/plugins/canvas/public/components/element_content/index.tsx @@ -5,23 +5,24 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getSelectedPage, getPageById } from '../../state/selectors/workpad'; -import { useExpressionsService } from '../../services'; import { ElementContent as Component, Props as ComponentProps } from './element_content'; import { State } from '../../../types'; +import { getCanvasExpressionService } from '../../services/canvas_expressions_service'; export type Props = Omit; export const ElementContent = (props: Props) => { - const expressionsService = useExpressionsService(); const selectedPageId = useSelector(getSelectedPage); const backgroundColor = useSelector((state: State) => getPageById(state, selectedPageId)?.style.background) || ''; const { renderable } = props; - const renderFunction = renderable ? expressionsService.getRenderer(renderable.as) : null; + const renderFunction = useMemo(() => { + return renderable ? getCanvasExpressionService().getRenderer(renderable.as) : null; + }, [renderable]); return ; }; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index 9adde7ab57718d..fbb7b971bfcb47 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -5,14 +5,18 @@ * 2.0. */ -import React, { FC, useCallback, useMemo } from 'react'; -import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React, { FC, useCallback, useMemo } from 'react'; -import { SavedObjectFinder, SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public'; -import { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common'; import { EmbeddableFactory, ReactEmbeddableSavedObject } from '@kbn/embeddable-plugin/public'; -import { useEmbeddablesService, usePlatformService } from '../../services'; +import { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common'; +import { SavedObjectFinder, SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public'; +import { + contentManagementService, + coreServices, + embeddableService, +} from '../../services/kibana_services'; const strings = { getNoItemsText: () => @@ -45,13 +49,8 @@ export const AddEmbeddableFlyout: FC = ({ onClose, isByValueEnabled, }) => { - const embeddablesService = useEmbeddablesService(); - const platformService = usePlatformService(); - const { getEmbeddableFactories, getReactEmbeddableSavedObjects } = embeddablesService; - const { getContentManagement, getUISettings } = platformService; - const legacyFactoriesBySavedObjectType: LegacyFactoryMap = useMemo(() => { - return [...getEmbeddableFactories()] + return [...embeddableService.getEmbeddableFactories()] .filter( (embeddableFactory) => Boolean(embeddableFactory.savedObjectMetaData?.type) && !embeddableFactory.isContainerType @@ -60,10 +59,10 @@ export const AddEmbeddableFlyout: FC = ({ acc[factory.savedObjectMetaData!.type] = factory; return acc; }, {} as LegacyFactoryMap); - }, [getEmbeddableFactories]); + }, []); const factoriesBySavedObjectType: FactoryMap = useMemo(() => { - return [...getReactEmbeddableSavedObjects()] + return [...embeddableService.getReactEmbeddableSavedObjects()] .filter(([type, embeddableFactory]) => { return Boolean(embeddableFactory.savedObjectMetaData?.type); }) @@ -74,7 +73,7 @@ export const AddEmbeddableFlyout: FC = ({ }; return acc; }, {} as FactoryMap); - }, [getReactEmbeddableSavedObjects]); + }, []); const metaData = useMemo( () => @@ -111,7 +110,7 @@ export const AddEmbeddableFlyout: FC = ({ onSelect(id, type, isByValueEnabled); return; } - const embeddableFactories = getEmbeddableFactories(); + const embeddableFactories = embeddableService.getEmbeddableFactories(); // Find the embeddable type from the saved object type const found = Array.from(embeddableFactories).find((embeddableFactory) => { return Boolean( @@ -124,7 +123,7 @@ export const AddEmbeddableFlyout: FC = ({ onSelect(id, foundEmbeddableType, isByValueEnabled); }, - [isByValueEnabled, getEmbeddableFactories, onSelect, factoriesBySavedObjectType] + [isByValueEnabled, onSelect, factoriesBySavedObjectType] ); return ( @@ -141,8 +140,8 @@ export const AddEmbeddableFlyout: FC = ({ showFilter={true} noItemsMessage={strings.getNoItemsText()} services={{ - contentClient: getContentManagement().client, - uiSettings: getUISettings(), + contentClient: contentManagementService.client, + uiSettings: coreServices.uiSettings, }} /> diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index d2772a04cdc2ec..3ecabeb8974f1d 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -15,7 +15,7 @@ import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; import { embeddableInputToExpression } from '../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression'; import { State } from '../../../types'; -import { useLabsService } from '../../services'; +import { presentationUtilService } from '../../services/kibana_services'; const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { @@ -67,8 +67,9 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ availableEmbeddables, ...restProps }) => { - const labsService = useLabsService(); - const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const isByValueEnabled = presentationUtilService.labsService.isProjectEnabled( + 'labs:canvas:byValueEmbeddable' + ); const dispatch = useDispatch(); const pageId = useSelector((state) => getSelectedPage(state)); diff --git a/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx b/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx index 4539b1f7274fcd..18a98630635cb3 100644 --- a/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx +++ b/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx @@ -7,14 +7,12 @@ import React, { FocusEventHandler } from 'react'; import { EuiComboBox } from '@elastic/eui'; -import { DataView } from '@kbn/data-views-plugin/common'; - -type DataViewOption = Pick; +import { DataViewListItem } from '@kbn/data-views-plugin/common'; export interface ESDataViewSelectProps { loading: boolean; value: string; - dataViews: DataViewOption[]; + dataViews: DataViewListItem[]; onChange: (string: string) => void; onBlur: FocusEventHandler | undefined; onFocus: FocusEventHandler | undefined; @@ -31,7 +29,7 @@ export const ESDataViewSelect: React.FunctionComponent = onFocus, onBlur, }) => { - const selectedDataView = dataViews.find((view) => value === view.title) as DataViewOption; + const selectedDataView = dataViews.find((view) => value === view.title); const selectedOption = selectedDataView ? { value: selectedDataView.title, label: selectedDataView.name || selectedDataView.title } diff --git a/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.tsx b/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.tsx index 271662755d32a8..217b7c79c8e754 100644 --- a/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.tsx +++ b/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.tsx @@ -5,25 +5,24 @@ * 2.0. */ -import { DataView } from '@kbn/data-views-plugin/common'; +import { DataViewListItem } from '@kbn/data-views-plugin/common'; import { sortBy } from 'lodash'; import React, { FC, useRef, useState } from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { useDataViewsService } from '../../services'; import { ESDataViewSelect as Component, ESDataViewSelectProps as Props, } from './es_data_view_select.component'; +import { getDataViews } from '../../lib/data_view_helpers'; type ESDataViewSelectProps = Omit; export const ESDataViewSelect: FC = (props) => { const { value, onChange } = props; - const [dataViews, setDataViews] = useState>>([]); + const [dataViews, setDataViews] = useState([]); const [loading, setLoading] = useState(true); const mounted = useRef(true); - const { getDataViews } = useDataViewsService(); useEffectOnce(() => { getDataViews().then((newDataViews) => { diff --git a/x-pack/plugins/canvas/public/components/es_field_select/index.tsx b/x-pack/plugins/canvas/public/components/es_field_select/index.tsx index 653eec22d77d9f..f41f67bb6ee953 100644 --- a/x-pack/plugins/canvas/public/components/es_field_select/index.tsx +++ b/x-pack/plugins/canvas/public/components/es_field_select/index.tsx @@ -6,8 +6,8 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { useDataViewsService } from '../../services'; import { ESFieldSelect as Component, ESFieldSelectProps as Props } from './es_field_select'; +import { getDataViewFields } from '../../lib/data_view_helpers'; type ESFieldSelectProps = Omit; @@ -15,17 +15,16 @@ export const ESFieldSelect: React.FunctionComponent = (props const { index, value, onChange } = props; const [fields, setFields] = useState([]); const loadingFields = useRef(false); - const { getFields } = useDataViewsService(); useEffect(() => { loadingFields.current = true; - getFields(index) + getDataViewFields(index) .then((newFields) => setFields(newFields || [])) .finally(() => { loadingFields.current = false; }); - }, [index, getFields]); + }, [index]); useEffect(() => { if (!loadingFields.current && value && !fields.includes(value)) { diff --git a/x-pack/plugins/canvas/public/components/es_fields_select/es_fields_select.tsx b/x-pack/plugins/canvas/public/components/es_fields_select/es_fields_select.tsx index c929203f9e094c..0cde66199b4b35 100644 --- a/x-pack/plugins/canvas/public/components/es_fields_select/es_fields_select.tsx +++ b/x-pack/plugins/canvas/public/components/es_fields_select/es_fields_select.tsx @@ -8,11 +8,11 @@ import React, { useState, useEffect, useRef } from 'react'; import { isEqual } from 'lodash'; import usePrevious from 'react-use/lib/usePrevious'; -import { useDataViewsService } from '../../services'; import { ESFieldsSelect as Component, ESFieldsSelectProps as Props, } from './es_fields_select.component'; +import { getDataViewFields } from '../../lib/data_view_helpers'; type ESFieldsSelectProps = Omit & { index: string }; @@ -21,11 +21,10 @@ export const ESFieldsSelect: React.FunctionComponent = (pro const [fields, setFields] = useState([]); const prevIndex = usePrevious(index); const mounted = useRef(true); - const { getFields } = useDataViewsService(); useEffect(() => { if (prevIndex !== index) { - getFields(index).then((newFields) => { + getDataViewFields(index).then((newFields) => { if (!mounted.current) { return; } @@ -37,7 +36,7 @@ export const ESFieldsSelect: React.FunctionComponent = (pro } }); } - }, [fields, index, onChange, prevIndex, selected, getFields]); + }, [fields, index, onChange, prevIndex, selected]); useEffect( () => () => { diff --git a/x-pack/plugins/canvas/public/components/expression/index.tsx b/x-pack/plugins/canvas/public/components/expression/index.tsx index 37bbbd1f5a4c1e..1d0c9915bf3471 100644 --- a/x-pack/plugins/canvas/public/components/expression/index.tsx +++ b/x-pack/plugins/canvas/public/components/expression/index.tsx @@ -8,7 +8,6 @@ import React, { FC, useState, useCallback, useMemo, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fromExpression } from '@kbn/interpreter'; -import { useExpressionsService } from '../../services'; import { getSelectedPage, getSelectedElement } from '../../state/selectors/workpad'; // @ts-expect-error import { setExpression, flushContext } from '../../state/actions/elements'; @@ -16,6 +15,7 @@ import { setExpression, flushContext } from '../../state/actions/elements'; import { ElementNotSelected } from './element_not_selected'; import { Expression as Component } from './expression'; import { State, CanvasElement } from '../../../types'; +import { getCanvasExpressionService } from '../../services/canvas_expressions_service'; interface ExpressionProps { done: () => void; @@ -45,7 +45,6 @@ export const Expression: FC = ({ done }) => { }; const ExpressionContainer: FC = ({ done, element, pageId }) => { - const expressions = useExpressionsService(); const dispatch = useDispatch(); const [isCompact, setCompact] = useState(true); const toggleCompactView = useCallback(() => { @@ -111,8 +110,8 @@ const ExpressionContainer: FC = ({ done, element, page }, [element, setFormState, formState]); const functionDefinitions = useMemo( - () => Object.values(expressions.getFunctions()), - [expressions] + () => Object.values(getCanvasExpressionService().getFunctions()), + [] ); return ( diff --git a/x-pack/plugins/canvas/public/components/function_form/index.tsx b/x-pack/plugins/canvas/public/components/function_form/index.tsx index 202bb4384d6961..f58eb496f7139c 100644 --- a/x-pack/plugins/canvas/public/components/function_form/index.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/index.tsx @@ -34,8 +34,8 @@ import { findExistingAsset } from '../../lib/find_existing_asset'; import { FunctionForm as Component } from './function_form'; import { Args, ArgType, ArgTypeDef } from '../../expression_types/types'; import { State, ExpressionContext, CanvasElement, AssetType } from '../../../types'; -import { useNotifyService, useWorkpadService } from '../../services'; import { createAsset, notifyError } from '../../lib/assets'; +import { getCanvasWorkpadService } from '../../services/canvas_workpad_service'; interface FunctionFormProps { name: string; @@ -54,8 +54,6 @@ interface FunctionFormProps { export const FunctionForm: React.FunctionComponent = (props) => { const { expressionIndex, ...restProps } = props; const { nextArgType, path, parentPath, argType } = restProps; - const service = useWorkpadService(); - const notifyService = useNotifyService(); const dispatch = useDispatch(); const context = useSelector( @@ -113,16 +111,16 @@ export const FunctionForm: React.FunctionComponent = (props) // make the ID here and pass it into the action const asset = createAsset(type, content); - return service + return getCanvasWorkpadService() .updateAssets(workpad.id, { ...workpad.assets, [asset.id]: asset }) .then((res) => { dispatch(setAsset(asset)); // then return the id, so the caller knows the id that will be created return asset.id; }) - .catch((error) => notifyError(error, notifyService.error)); + .catch((error) => notifyError(error)); }, - [dispatch, notifyService, service, workpad.assets, workpad.id] + [dispatch, workpad.assets, workpad.id] ); const onAssetAdd = useCallback( diff --git a/x-pack/plugins/canvas/public/components/function_form_list/index.js b/x-pack/plugins/canvas/public/components/function_form_list/index.js index 0ad6651e3f57e9..6de7b81b12b01e 100644 --- a/x-pack/plugins/canvas/public/components/function_form_list/index.js +++ b/x-pack/plugins/canvas/public/components/function_form_list/index.js @@ -8,7 +8,7 @@ import { compose, withProps } from 'react-recompose'; import { get } from 'lodash'; import { toExpression } from '@kbn/interpreter'; -import { pluginServices } from '../../services'; +import { getCanvasExpressionService } from '../../services/canvas_expressions_service'; import { getArgTypeDef } from '../../lib/args'; import { FunctionFormList as Component } from './function_form_list'; @@ -78,7 +78,7 @@ const componentFactory = ({ parentPath, removable, }) => { - const { expressions } = pluginServices.getServices(); + const expressions = getCanvasExpressionService(); return { args, nestedFunctionsArgs: argsWithExprFunctions, diff --git a/x-pack/plugins/canvas/public/components/home/home.stories.tsx b/x-pack/plugins/canvas/public/components/home/home.stories.tsx index 0130f9f3f894ba..cfebd509c80ef0 100644 --- a/x-pack/plugins/canvas/public/components/home/home.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { reduxDecorator } from '../../../storybook'; -import { argTypes } from '../../services/storybook'; +import { argTypes } from '../../../storybook/constants'; import { Home } from './home'; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts index 001a711a58a726..90a7c4c33de899 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts @@ -9,16 +9,17 @@ import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { useNotifyService, useWorkpadService } from '../../../services'; +import { useNotifyService } from '../../../services'; import { getId } from '../../../lib/get_id'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; export const useCloneWorkpad = () => { - const workpadService = useWorkpadService(); const notifyService = useNotifyService(); const history = useHistory(); return useCallback( async (workpadId: string) => { + const workpadService = getCanvasWorkpadService(); try { let workpad = await workpadService.get(workpadId); @@ -35,7 +36,7 @@ export const useCloneWorkpad = () => { notifyService.error(err, { title: errors.getCloneFailureErrorMessage() }); } }, - [notifyService, workpadService, history] + [notifyService, history] ); }; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts index 968f9398ba8577..6b57cf61ca905e 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts @@ -9,15 +9,16 @@ import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { CanvasTemplate } from '../../../../types'; -import { useNotifyService, useWorkpadService } from '../../../services'; +import { useNotifyService } from '../../../services'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; export const useCreateFromTemplate = () => { - const workpadService = useWorkpadService(); const notifyService = useNotifyService(); const history = useHistory(); return useCallback( async (template: CanvasTemplate) => { + const workpadService = getCanvasWorkpadService(); try { const result = await workpadService.createFromTemplate(template.id); history.push(`/workpad/${result.id}/page/1`); @@ -27,6 +28,6 @@ export const useCreateFromTemplate = () => { }); } }, - [workpadService, notifyService, history] + [notifyService, history] ); }; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts index 3290bc8227a292..f950a4adcd037a 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts @@ -11,17 +11,18 @@ import { i18n } from '@kbn/i18n'; // @ts-expect-error import { getDefaultWorkpad } from '../../../state/defaults'; -import { useNotifyService, useWorkpadService } from '../../../services'; +import { useNotifyService } from '../../../services'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; import type { CanvasWorkpad } from '../../../../types'; export const useCreateWorkpad = () => { - const workpadService = useWorkpadService(); const notifyService = useNotifyService(); const history = useHistory(); return useCallback( async (_workpad?: CanvasWorkpad | null) => { + const workpadService = getCanvasWorkpadService(); const workpad = _workpad || (getDefaultWorkpad() as CanvasWorkpad); try { @@ -34,7 +35,7 @@ export const useCreateWorkpad = () => { } return; }, - [notifyService, history, workpadService] + [notifyService, history] ); }; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts index 722ddae7411c92..dc52fe2e82d5cb 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts @@ -8,51 +8,48 @@ import { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { useNotifyService, useWorkpadService } from '../../../services'; +import { getCanvasNotifyService } from '../../../services/canvas_notify_service'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; export const useDeleteWorkpads = () => { - const workpadService = useWorkpadService(); - const notifyService = useNotifyService(); - - return useCallback( - async (workpadIds: string[]) => { - const removedWorkpads = workpadIds.map(async (id) => { - try { - await workpadService.remove(id); - return { id, err: null }; - } catch (err) { - return { id, err }; - } - }); - - return Promise.all(removedWorkpads).then((results) => { - const [passes, errored] = results.reduce<[string[], string[]]>( - ([passesArr, errorsArr], result) => { - if (result.err) { - errorsArr.push(result.id); - } else { - passesArr.push(result.id); - } - - return [passesArr, errorsArr]; - }, - [[], []] - ); - - const removedIds = workpadIds.filter((id) => passes.includes(id)); - - if (errored.length > 0) { - notifyService.error(errors.getDeleteFailureErrorMessage()); - } - - return { - removedIds, - errored, - }; - }); - }, - [workpadService, notifyService] - ); + return useCallback(async (workpadIds: string[]) => { + const workpadService = getCanvasWorkpadService(); + + const removedWorkpads = workpadIds.map(async (id) => { + try { + await workpadService.remove(id); + return { id, err: null }; + } catch (err) { + return { id, err }; + } + }); + + return Promise.all(removedWorkpads).then((results) => { + const [passes, errored] = results.reduce<[string[], string[]]>( + ([passesArr, errorsArr], result) => { + if (result.err) { + errorsArr.push(result.id); + } else { + passesArr.push(result.id); + } + + return [passesArr, errorsArr]; + }, + [[], []] + ); + + const removedIds = workpadIds.filter((id) => passes.includes(id)); + + if (errored.length > 0) { + getCanvasNotifyService().error(errors.getDeleteFailureErrorMessage()); + } + + return { + removedIds, + errored, + }; + }); + }, []); }; const errors = { diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts index 9364a79987908d..426b612774762f 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts @@ -6,10 +6,8 @@ */ import { useCallback } from 'react'; - -import { useWorkpadService } from '../../../services'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; export const useFindTemplates = () => { - const workpadService = useWorkpadService(); - return useCallback(async () => await workpadService.findTemplates(), [workpadService]); + return useCallback(async () => await getCanvasWorkpadService().findTemplates(), []); }; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts index 10352d0472e8c3..59ff5cd0561b5b 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts @@ -8,21 +8,21 @@ import { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { useNotifyService, useWorkpadService } from '../../../services'; +import { useNotifyService } from '../../../services'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; export const useFindWorkpads = () => { - const workpadService = useWorkpadService(); const notifyService = useNotifyService(); return useCallback( async (text = '') => { try { - return await workpadService.find(text); + return await getCanvasWorkpadService().find(text); } catch (err) { notifyService.error(err, { title: errors.getFindFailureErrorMessage() }); } }, - [notifyService, workpadService] + [notifyService] ); }; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts index bd780ee01507d4..bb3f447cd107b7 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts @@ -9,17 +9,18 @@ import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { useNotifyService, useWorkpadService } from '../../../services'; +import { useNotifyService } from '../../../services'; import type { CanvasWorkpad } from '../../../../types'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; export const useImportWorkpad = () => { - const workpadService = useWorkpadService(); const notifyService = useNotifyService(); const history = useHistory(); return useCallback( async (workpad: CanvasWorkpad) => { + const workpadService = getCanvasWorkpadService(); try { const importedWorkpad = await workpadService.import(workpad); history.push(`/workpad/${importedWorkpad.id}/page/1`); @@ -30,7 +31,7 @@ export const useImportWorkpad = () => { } return; }, - [notifyService, history, workpadService] + [notifyService, history] ); }; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx index d9e3f0e4e2c999..6f25bbf8720cfa 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FoundWorkpad } from '../../../services/workpad'; +import { FoundWorkpad } from '../../../services/canvas_workpad_service'; import { UploadDropzone } from './upload_dropzone'; import { HomeEmptyPrompt } from './empty_prompt'; import { WorkpadTable } from './workpad_table'; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx index 52afd552bbc490..967419e9b8b06f 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { reduxDecorator } from '../../../../storybook'; -import { argTypes } from '../../../services/storybook'; +import { argTypes } from '../../../../storybook/constants'; import { MyWorkpads as Component } from './my_workpads'; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx index b75c8864ad495c..44b3c5938af6cb 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx @@ -7,7 +7,7 @@ import React, { useState, useEffect, createContext, Dispatch, SetStateAction } from 'react'; import { useFindWorkpads } from '../hooks'; -import { FoundWorkpad } from '../../../services/workpad'; +import { FoundWorkpad } from '../../../services/canvas_workpad_service'; import { Loading } from '../loading'; import { MyWorkpads as Component } from './my_workpads.component'; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx index c64ab50ad8a096..f8e349351ba4c2 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx @@ -21,7 +21,7 @@ import { import moment from 'moment'; import { RoutingLink } from '../../routing'; -import { FoundWorkpad } from '../../../services/workpad'; +import { FoundWorkpad } from '../../../services/canvas_workpad_service'; import { WorkpadTableTools } from './workpad_table_tools'; import { WorkpadImport } from './workpad_import'; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx index 6675dea238cc44..f9418f35e62566 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx @@ -9,9 +9,9 @@ import React, { useState, useEffect } from 'react'; import { EuiPanel } from '@elastic/eui'; import { reduxDecorator } from '../../../../storybook'; +import { argTypes } from '../../../../storybook/constants'; -import { argTypes } from '../../../services/storybook'; -import { getSomeWorkpads } from '../../../services/stubs/workpad'; +import { getSomeWorkpads } from '../../../services/mocks'; import { WorkpadTable as Component } from './workpad_table'; import { WorkpadsContext } from './my_workpads'; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx index 6d88691f2eabe5..7bbb3a57ab9ced 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx @@ -8,17 +8,16 @@ import React, { useContext } from 'react'; import { useSelector } from 'react-redux'; -import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; import type { State } from '../../../../types'; -import { usePlatformService } from '../../../services'; -import { useCloneWorkpad } from '../hooks'; +import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; import { useDownloadWorkpad } from '../../hooks'; +import { useCloneWorkpad } from '../hooks'; -import { WorkpadTable as Component } from './workpad_table.component'; +import { coreServices } from '../../../services/kibana_services'; import { WorkpadsContext } from './my_workpads'; +import { WorkpadTable as Component } from './workpad_table.component'; export const WorkpadTable = () => { - const platformService = usePlatformService(); const onCloneWorkpad = useCloneWorkpad(); const onExportWorkpad = useDownloadWorkpad(); const context = useContext(WorkpadsContext); @@ -33,7 +32,7 @@ export const WorkpadTable = () => { const { workpads } = context; - const dateFormat = platformService.getUISetting('dateFormat'); + const dateFormat = coreServices.uiSettings.get('dateFormat'); return ; }; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx index 251a40ff4d45d1..148e1973352bc2 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, EuiToolTip, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { ConfirmModal } from '../../confirm_modal'; -import { FoundWorkpad } from '../../../services/workpad'; +import { FoundWorkpad } from '../../../services/canvas_workpad_service'; export interface Props { workpads: FoundWorkpad[]; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx index 92583ca845aa80..c6a3347f6c39ae 100644 --- a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx @@ -9,7 +9,7 @@ import { EuiPanel } from '@elastic/eui'; import React from 'react'; import { reduxDecorator } from '../../../../storybook'; -import { argTypes } from '../../../services/storybook'; +import { argTypes } from '../../../../storybook/constants'; import { WorkpadTemplates as Component } from './workpad_templates'; diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx index 086a737d525e9f..54d9659f5a1d8b 100644 --- a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx @@ -11,17 +11,16 @@ import { useDispatch } from 'react-redux'; import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; import { resetWorkpad } from '../../state/actions/workpad'; import { HomeApp as Component } from './home_app.component'; -import { usePlatformService } from '../../services'; +import { coreServices } from '../../services/kibana_services'; export const HomeApp = () => { - const { setBreadcrumbs } = usePlatformService(); const dispatch = useDispatch(); const onLoad = () => dispatch(resetWorkpad()); const history = useHistory(); useEffect(() => { - setBreadcrumbs([getBaseBreadcrumb(history)]); - }, [setBreadcrumbs, history]); + coreServices.chrome.setBreadcrumbs([getBaseBreadcrumb(history)]); + }, [history]); return ; }; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/use_download_workpad.ts b/x-pack/plugins/canvas/public/components/hooks/workpad/use_download_workpad.ts index dadf03a8fac5a8..394274b1eb133d 100644 --- a/x-pack/plugins/canvas/public/components/hooks/workpad/use_download_workpad.ts +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/use_download_workpad.ts @@ -8,9 +8,10 @@ import { useCallback } from 'react'; import fileSaver from 'file-saver'; import { i18n } from '@kbn/i18n'; -import { useNotifyService, useWorkpadService } from '../../../services'; +import { useNotifyService } from '../../../services'; import { CanvasWorkpad } from '../../../../types'; import type { CanvasRenderedWorkpad } from '../../../../shareable_runtime/types'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; const strings = { getDownloadFailureErrorMessage: () => @@ -28,12 +29,12 @@ const strings = { export const useDownloadWorkpad = () => { const notifyService = useNotifyService(); - const workpadService = useWorkpadService(); const download = useDownloadWorkpadBlob(); return useCallback( async (workpadId: string) => { try { + const workpadService = getCanvasWorkpadService(); const workpad = await workpadService.get(workpadId); download(workpad, `canvas-workpad-${workpad.name}-${workpad.id}`); @@ -41,7 +42,7 @@ export const useDownloadWorkpad = () => { notifyService.error(err, { title: strings.getDownloadFailureErrorMessage() }); } }, - [workpadService, notifyService, download] + [notifyService, download] ); }; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts index 0a4e66917814dc..50c3e527bbbae6 100644 --- a/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts @@ -12,7 +12,7 @@ import { ErrorStrings } from '../../../../i18n'; import { CANVAS_APP } from '../../../../common/lib'; import { decode } from '../../../../common/lib/embeddable_dataurl'; import { CanvasElement, CanvasPage } from '../../../../types'; -import { useEmbeddablesService, useLabsService, useNotifyService } from '../../../services'; +import { useNotifyService } from '../../../services'; // @ts-expect-error unconverted file import { addElement, fetchAllRenderables } from '../../../state/actions/elements'; // @ts-expect-error unconverted file @@ -24,16 +24,16 @@ import { } from '../../../state/actions/embeddable'; import { clearValue } from '../../../state/actions/resolved_args'; import { embeddableInputToExpression } from '../../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression'; +import { embeddableService, presentationUtilService } from '../../../services/kibana_services'; const { actionsElements: strings } = ErrorStrings; export const useIncomingEmbeddable = (selectedPage: CanvasPage) => { - const embeddablesService = useEmbeddablesService(); - const labsService = useLabsService(); + const labsService = presentationUtilService.labsService; const dispatch = useDispatch(); const notifyService = useNotifyService(); const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); - const stateTransferService = embeddablesService.getStateTransfer(); + const stateTransferService = embeddableService.getStateTransfer(); // fetch incoming embeddable from state transfer service. const incomingEmbeddable = stateTransferService.getIncomingEmbeddablePackage(CANVAS_APP, true); diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx index 19e786edfd5fb0..5aeb8847ab8873 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { camelCase } from 'lodash'; import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import { useNotifyService, useCustomElementService } from '../../services'; +import { useNotifyService } from '../../services'; // @ts-expect-error untyped local import { selectToplevelNodes } from '../../state/actions/transient'; // @ts-expect-error untyped local @@ -21,6 +21,7 @@ import { Props as ComponentProps, } from './saved_elements_modal.component'; import { PositionedElement, CustomElement } from '../../../types'; +import { getCustomElementService } from '../../services/canvas_custom_element_service'; const customElementAdded = 'elements-custom-added'; @@ -28,7 +29,7 @@ export type Props = Pick; export const SavedElementsModal = ({ onClose }: Props) => { const notifyService = useNotifyService(); - const customElementService = useCustomElementService(); + const customElementService = useMemo(() => getCustomElementService(), []); const dispatch = useDispatch(); const pageId = useSelector(getSelectedPage); const [customElements, setCustomElements] = useState([]); diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx index 1e604a6deb8508..6052e6951a8e91 100644 --- a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx @@ -24,10 +24,10 @@ import { useZoomHandlers } from '../../lib/app_handler_creators'; import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; import { LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY } from '../../../common/lib/constants'; import { WorkpadRoutingContext } from '../../routes/workpad'; -import { usePlatformService } from '../../services'; import { Workpad as WorkpadComponent, Props } from './workpad.component'; import { State } from '../../../types'; import { useIncomingEmbeddable } from '../hooks'; +import { coreServices } from '../../services/kibana_services'; type ContainerProps = Pick; @@ -40,9 +40,7 @@ export const Workpad: FC = (props) => { const { isFullscreen, setFullscreen, undo, redo, autoplayInterval, nextPage, previousPage } = useContext(WorkpadRoutingContext); - const platformService = usePlatformService(); - - const hasHeaderBanner = useObservable(platformService.hasHeaderBanner$()); + const hasHeaderBanner = useObservable(coreServices.chrome.hasHeaderBanner$()); const propsFromState = useSelector((state: State) => { const { width, height, id: workpadId, css: workpadCss } = getWorkpad(state); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts index bdccc8040c5def..9aa9aecdfd5169 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts +++ b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts @@ -5,17 +5,19 @@ * 2.0. */ +import { useMemo } from 'react'; import { AstFunction, fromExpression } from '@kbn/interpreter'; import { shallowEqual, useSelector } from 'react-redux'; + import { State } from '../../../../types'; import { getFiltersByFilterExpressions } from '../../../lib/filter'; import { adaptCanvasFilter } from '../../../lib/filter_adapters'; -import { useFiltersService } from '../../../services'; +import { getCanvasFiltersService } from '../../../services/canvas_filters_service'; const extractExpressionAST = (filters: string[]) => fromExpression(filters.join(' | ')); export function useCanvasFilters(filterExprsToGroupBy: AstFunction[] = []) { - const filtersService = useFiltersService(); + const filtersService = useMemo(() => getCanvasFiltersService(), []); const filterExpressions = useSelector( (state: State) => filtersService.getFilters(state), shallowEqual diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx index fd644903ac25da..06d20e919dcbe8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -21,11 +21,6 @@ import { import { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public/actions'; import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; -import { - useEmbeddablesService, - useUiActionsService, - useVisualizationsService, -} from '../../../services'; import { CANVAS_APP } from '../../../../common/lib'; import { ElementSpec } from '../../../../types'; import { EditorMenu as Component } from './editor_menu.component'; @@ -33,6 +28,11 @@ import { embeddableInputToExpression } from '../../../../canvas_plugin_src/rende import { EmbeddableInput as CanvasEmbeddableInput } from '../../../../canvas_plugin_src/expression_types'; import { useCanvasApi } from '../../hooks/use_canvas_api'; import { ADD_CANVAS_ELEMENT_TRIGGER } from '../../../state/triggers/add_canvas_element_trigger'; +import { + embeddableService, + uiActionsService, + visualizationsService, +} from '../../../services/kibana_services'; interface Props { /** @@ -47,18 +47,15 @@ interface UnwrappedEmbeddableFactory { } export const EditorMenu: FC = ({ addElement }) => { - const embeddablesService = useEmbeddablesService(); const { pathname, search, hash } = useLocation(); - const stateTransferService = embeddablesService.getStateTransfer(); - const visualizationsService = useVisualizationsService(); - const uiActions = useUiActionsService(); + const stateTransferService = embeddableService.getStateTransfer(); const canvasApi = useCanvasApi(); const [addPanelActions, setAddPanelActions] = useState>>([]); const embeddableFactories = useMemo( - () => (embeddablesService ? Array.from(embeddablesService.getEmbeddableFactories()) : []), - [embeddablesService] + () => (embeddableService ? Array.from(embeddableService.getEmbeddableFactories()) : []), + [] ); const [unwrappedEmbeddableFactories, setUnwrappedEmbeddableFactories] = useState< @@ -79,7 +76,7 @@ export const EditorMenu: FC = ({ addElement }) => { useEffect(() => { let mounted = true; async function loadPanelActions() { - const registeredActions = await uiActions?.getTriggerCompatibleActions?.( + const registeredActions = await uiActionsService.getTriggerCompatibleActions( ADD_CANVAS_ELEMENT_TRIGGER, { embeddable: canvasApi } ); @@ -89,7 +86,7 @@ export const EditorMenu: FC = ({ addElement }) => { return () => { mounted = false; }; - }, [uiActions, canvasApi]); + }, [canvasApi]); const createNewVisType = useCallback( (visType?: BaseVisType | VisTypeAlias) => () => { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx index 75d6f8064f32a6..6e6ca58f0dd72f 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import React, { useState } from 'react'; import { EuiButtonEmpty, EuiNotificationBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; import { LazyLabsFlyout, withSuspense } from '@kbn/presentation-util-plugin/public'; -import { useLabsService } from '../../../services'; +import { UI_SETTINGS } from '../../../../common'; +import { coreServices, presentationUtilService } from '../../../services/kibana_services'; const strings = { getLabsButtonLabel: () => @@ -23,14 +24,13 @@ const strings = { const Flyout = withSuspense(LazyLabsFlyout, null); export const LabsControl = () => { - const { isLabsEnabled, getProjects } = useLabsService(); const [isShown, setIsShown] = useState(false); - if (!isLabsEnabled()) { + if (!coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI)) { return null; } - const projects = getProjects(['canvas']); + const projects = presentationUtilService.labsService.getProjects(['canvas']); const overrideCount = Object.values(projects).filter( (project) => project.status.isOverride ).length; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/use_download_runtime.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/use_download_runtime.ts index cd1635d65a573a..49bd93c290ce88 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/use_download_runtime.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/use_download_runtime.ts @@ -5,14 +5,16 @@ * 2.0. */ -import { useCallback } from 'react'; -import fileSaver from 'file-saver'; import { i18n } from '@kbn/i18n'; +import fileSaver from 'file-saver'; +import { useCallback } from 'react'; import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../../../../../common/lib/constants'; import { ZIP } from '../../../../../../i18n/constants'; -import { usePlatformService, useNotifyService, useWorkpadService } from '../../../../../services'; import type { CanvasRenderedWorkpad } from '../../../../../../shareable_runtime/types'; +import { useNotifyService } from '../../../../../services'; +import { coreServices } from '../../../../../services/kibana_services'; +import { getCanvasWorkpadService } from '../../../../../services/canvas_workpad_service'; const strings = { getDownloadRuntimeFailureErrorMessage: () => @@ -35,28 +37,28 @@ const strings = { }; export const useDownloadRuntime = () => { - const platformService = usePlatformService(); const notifyService = useNotifyService(); const downloadRuntime = useCallback(() => { try { - const path = `${platformService.getBasePath()}${API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD}`; + const path = `${coreServices.http.basePath.get()}${API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD}`; window.open(path); return; } catch (err) { notifyService.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); } - }, [platformService, notifyService]); + }, [notifyService]); return downloadRuntime; }; export const useDownloadZippedRuntime = () => { - const workpadService = useWorkpadService(); const notifyService = useNotifyService(); const downloadZippedRuntime = useCallback( (workpad: CanvasRenderedWorkpad) => { + const workpadService = getCanvasWorkpadService(); + const downloadZip = async () => { try { let runtimeZipBlob: Blob | undefined; @@ -80,7 +82,7 @@ export const useDownloadZippedRuntime = () => { downloadZip(); }, - [notifyService, workpadService] + [notifyService] ); return downloadZippedRuntime; }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx index c66336a9153c03..e4239864c1915c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -9,11 +9,11 @@ import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { State } from '../../../../types'; -import { useReportingService, usePlatformService } from '../../../services'; import { getPages, getWorkpad } from '../../../state/selectors/workpad'; import { useDownloadWorkpad } from '../../hooks'; import { ShareMenu as ShareMenuComponent } from './share_menu.component'; import { getPdfJobParams } from './utils'; +import { kibanaVersion, reportingService } from '../../../services/kibana_services'; const strings = { getUnknownExportErrorMessage: (type: string) => @@ -27,32 +27,30 @@ const strings = { export const ShareMenu = () => { const downloadWorkpad = useDownloadWorkpad(); - const reportingService = useReportingService(); - const platformService = usePlatformService(); const { workpad, pageCount } = useSelector((state: State) => ({ workpad: getWorkpad(state), pageCount: getPages(state).length, })); - const ReportingPanelPDFComponent = reportingService.getReportingPanelPDFComponent(); - const sharingData = { workpad, pageCount, }; - const ReportingComponent = - ReportingPanelPDFComponent !== null - ? ({ onClose }: { onClose: () => void }) => ( - getPdfJobParams(sharingData, platformService.getKibanaVersion())} + const ReportingComponent = reportingService + ? ({ onClose }: { onClose: () => void }) => { + const ReportingPanelPDFV2 = reportingService!.components.ReportingPanelPDFV2; + return ( + getPdfJobParams(sharingData, kibanaVersion)} layoutOption="canvas" onClose={onClose} objectId={workpad.id} /> - ) - : null; + ); + } + : null; const onExport = useCallback( (type: string) => { diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index a168020b6eef87..a37953657e1572 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -7,10 +7,11 @@ import { fromExpression } from '@kbn/interpreter'; import { get } from 'lodash'; -import { pluginServices } from '../services'; import type { FiltersFunction } from '../../common/functions'; import { buildFiltersFunction } from '../../common/functions'; import { InitializeArguments } from '.'; +import { getCanvasFiltersService } from '../services/canvas_filters_service'; +import { getCanvasExpressionService } from '../services/canvas_expressions_service'; export interface Arguments { group: string[]; @@ -40,7 +41,8 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = export function filtersFunctionFactory(initialize: InitializeArguments): () => FiltersFunction { const fn: FiltersFunction['fn'] = (input, { group, ungrouped }) => { - const { expressions, filters: filtersService } = pluginServices.getServices(); + const expressions = getCanvasExpressionService(); + const filtersService = getCanvasFiltersService(); const filterList = getFiltersByGroup(filtersService.getFilters(), group, ungrouped); diff --git a/x-pack/plugins/canvas/public/lib/assets.ts b/x-pack/plugins/canvas/public/lib/assets.ts index d7fd0ecbac57fd..b51cda9216fbce 100644 --- a/x-pack/plugins/canvas/public/lib/assets.ts +++ b/x-pack/plugins/canvas/public/lib/assets.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; import { AssetType, CanvasAsset } from '../../types'; -import { CanvasNotifyService } from '../services/notify'; import { getId } from './get_id'; +import { getCanvasNotifyService } from '../services/canvas_notify_service'; const strings = { getSaveFailureTitle: () => @@ -32,7 +32,8 @@ export const createAsset = (type: AssetType['type'], content: AssetType['value'] '@created': new Date().toISOString(), }); -export const notifyError = (err: any, notifyErrorFn: CanvasNotifyService['error']) => { +export const notifyError = (err: any) => { + const { error: notifyErrorFn } = getCanvasNotifyService(); const statusCode = err.response && err.response.status; switch (statusCode) { case 400: diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 374bdaff997213..8b78914d770718 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -12,8 +12,10 @@ import { } from '@kbn/expressions-plugin/public'; import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable'; import { RendererHandlers, CanvasElement } from '../../types'; -import { pluginServices } from '../services'; +import { getCanvasFiltersService } from '../services/canvas_filters_service'; import { clearValue } from '../state/actions/resolved_args'; +// @ts-expect-error unconverted file +import { fetchAllRenderables } from '../state/actions/elements'; // This class creates stub handlers to ensure every element and renderer fulfills the contract. // TODO: consider warning if these methods are invoked but not implemented by the renderer...? @@ -80,7 +82,7 @@ export const createDispatchedHandlerFactory = ( oldElement = element; } - const { filters } = pluginServices.getServices(); + const filters = getCanvasFiltersService(); const handlers: RendererHandlers & { event: IInterpreterRenderHandlers['event']; @@ -94,6 +96,7 @@ export const createDispatchedHandlerFactory = ( break; case 'applyFilterAction': filters.updateFilter(element.id, event.data); + dispatch(fetchAllRenderables()); break; case 'onComplete': this.onComplete(event.data); diff --git a/x-pack/plugins/canvas/public/lib/data_view_helpers.ts b/x-pack/plugins/canvas/public/lib/data_view_helpers.ts new file mode 100644 index 00000000000000..0bec688f6788ae --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/data_view_helpers.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { dataViewsService } from '../services/kibana_services'; +import { getCanvasNotifyService } from '../services/canvas_notify_service'; +import { ErrorStrings } from '../../i18n'; + +export const getDataViews = async () => { + try { + return await dataViewsService.getIdsWithTitle(); + } catch (e) { + const { esService: strings } = ErrorStrings; + getCanvasNotifyService().error(e, { title: strings.getIndicesFetchErrorMessage() }); + } + return []; +}; + +export const getDataViewFields = async (dataViewTitle: string) => { + const dataView = await dataViewsService.create({ title: dataViewTitle }); + + return dataView.fields.filter((field) => !field.name.startsWith('_')).map((field) => field.name); +}; diff --git a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts index 82b98275290408..72a7df3a267754 100644 --- a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts +++ b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts @@ -8,10 +8,11 @@ import { camelCase } from 'lodash'; import { getClipboardData, setClipboardData } from './clipboard'; import { cloneSubgraphs } from './clone_subgraphs'; -import { pluginServices } from '../services'; import { getId } from './get_id'; import { PositionedElement } from '../../types'; import { ELEMENT_NUDGE_OFFSET, ELEMENT_SHIFT_OFFSET } from '../../common/lib/constants'; +import { getCanvasNotifyService } from '../services/canvas_notify_service'; +import { getCustomElementService } from '../services/canvas_custom_element_service'; const extractId = (node: { id: string }): string => node.id; @@ -71,8 +72,8 @@ export const basicHandlerCreators = { createCustomElement: ({ selectedNodes }: Props) => (name = '', description = '', image = ''): void => { - const notifyService = pluginServices.getServices().notify; - const customElementService = pluginServices.getServices().customElement; + const notifyService = getCanvasNotifyService(); + const customElementService = getCustomElementService(); if (selectedNodes.length) { const content = JSON.stringify({ selectedNodes }); @@ -145,7 +146,7 @@ export const clipboardHandlerCreators = { cutNodes: ({ pageId, removeNodes, selectedNodes }: Props) => (): void => { - const notifyService = pluginServices.getServices().notify; + const notifyService = getCanvasNotifyService(); if (selectedNodes.length) { setClipboardData({ selectedNodes }); @@ -156,7 +157,7 @@ export const clipboardHandlerCreators = { copyNodes: ({ selectedNodes }: Props) => (): void => { - const notifyService = pluginServices.getServices().notify; + const notifyService = getCanvasNotifyService(); if (selectedNodes.length) { setClipboardData({ selectedNodes }); diff --git a/x-pack/plugins/canvas/public/lib/fullscreen.js b/x-pack/plugins/canvas/public/lib/fullscreen.js index fd4e0b65785b98..50a9578b07b782 100644 --- a/x-pack/plugins/canvas/public/lib/fullscreen.js +++ b/x-pack/plugins/canvas/public/lib/fullscreen.js @@ -5,22 +5,21 @@ * 2.0. */ -import { pluginServices } from '../services'; +import { coreServices } from '../services/kibana_services'; export const fullscreenClass = 'canvas-isFullscreen'; export function setFullscreen(fullscreen, doc = document) { - const platformService = pluginServices.getServices().platform; const enabled = Boolean(fullscreen); const body = doc.querySelector('body'); const bodyClassList = body.classList; const isFullscreen = bodyClassList.contains(fullscreenClass); if (enabled && !isFullscreen) { - platformService.setFullscreen(false); + coreServices.chrome.setIsVisible(false); bodyClassList.add(fullscreenClass); } else if (!enabled && isFullscreen) { bodyClassList.remove(fullscreenClass); - platformService.setFullscreen(true); + coreServices.chrome.setIsVisible(true); } } diff --git a/x-pack/plugins/canvas/public/lib/template_service.ts b/x-pack/plugins/canvas/public/lib/template_service.ts index d5ec467f18740e..481733b781d853 100644 --- a/x-pack/plugins/canvas/public/lib/template_service.ts +++ b/x-pack/plugins/canvas/public/lib/template_service.ts @@ -9,12 +9,11 @@ import { API_ROUTE_TEMPLATES } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; -import { pluginServices } from '../services'; import { CanvasTemplate } from '../../types'; +import { coreServices } from '../services/kibana_services'; const getApiPath = function () { - const platformService = pluginServices.getServices().platform; - const basePath = platformService.getBasePath(); + const basePath = coreServices.http.basePath.get(); return `${basePath}${API_ROUTE_TEMPLATES}`; }; diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 5f385ce5f079b0..bd4e920a56f7e6 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -38,6 +38,7 @@ import { initLoadingIndicator } from './lib/loading_indicator'; import { getPluginApi, CanvasApi } from './plugin_api'; import { setupExpressions } from './setup_expressions'; import { addCanvasElementTrigger } from './state/triggers/add_canvas_element_trigger'; +import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services'; export type { CoreStart, CoreSetup }; @@ -121,22 +122,13 @@ export class CanvasPlugin setupExpressions({ coreSetup, setupPlugins }); // Get start services - const [coreStart, startPlugins] = await coreSetup.getStartServices(); + const [[coreStart, startPlugins]] = await Promise.all([ + coreSetup.getStartServices(), + untilPluginStartServicesReady(), + ]); srcPlugin.start(coreStart, startPlugins); - const { pluginServices } = await import('./services'); - const { pluginServiceRegistry } = await import('./services/kibana'); - - pluginServices.setRegistry( - pluginServiceRegistry.start({ - coreStart, - startPlugins, - appUpdater: this.appUpdater, - initContext: this.initContext, - }) - ); - const { expressions, presentationUtil } = startPlugins; await presentationUtil.registerExpressionsLanguage( Object.values(expressions.getFunctions()) @@ -154,7 +146,13 @@ export class CanvasPlugin this.appUpdater ); - const unmount = renderApp({ coreStart, startPlugins, params, canvasStore, pluginServices }); + const unmount = renderApp({ + coreStart, + startPlugins, + params, + canvasStore, + appUpdater: this.appUpdater, + }); return () => { unmount(); @@ -190,6 +188,7 @@ export class CanvasPlugin } public start(coreStart: CoreStart, startPlugins: CanvasStartDeps) { + setKibanaServices(coreStart, startPlugins, this.initContext); initLoadingIndicator(coreStart.http.addLoadingCountSource); } } diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts index ca66fa227e4eb0..e517f74030c87b 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts @@ -6,32 +6,30 @@ */ import { useContext, useEffect } from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { usePlatformService } from '../../../services'; import { WorkpadRoutingContext } from '..'; +import { coreServices } from '../../../services/kibana_services'; const fullscreenClass = 'canvas-isFullscreen'; export const useFullscreenPresentationHelper = () => { const { isFullscreen } = useContext(WorkpadRoutingContext); - const { setFullscreen } = usePlatformService(); - useEffect(() => { const body = document.querySelector('body'); const bodyClassList = body!.classList; const hasFullscreenClass = bodyClassList.contains(fullscreenClass); if (isFullscreen && !hasFullscreenClass) { - setFullscreen(false); + coreServices.chrome.setIsVisible(false); bodyClassList.add(fullscreenClass); } else if (!isFullscreen && hasFullscreenClass) { bodyClassList.remove(fullscreenClass); - setFullscreen(true); + coreServices.chrome.setIsVisible(true); } - }, [isFullscreen, setFullscreen]); + }, [isFullscreen]); // Remove fullscreen when component unmounts useEffectOnce(() => () => { - setFullscreen(true); + coreServices.chrome.setIsVisible(true); document.querySelector('body')?.classList.remove(fullscreenClass); }); }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx index 0d73fe49601c82..1ba0dacd8c1432 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx @@ -7,6 +7,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useWorkpad } from './use_workpad'; +import { spacesService } from '../../../services/kibana_services'; const mockDispatch = jest.fn(); const mockSelector = jest.fn(); @@ -25,21 +26,22 @@ const workpadResponse = { assets, }; -// Mock the hooks and actions used by the UseWorkpad hook +// Mock the hooks, actions, and services used by the UseWorkpad hook jest.mock('react-redux', () => ({ useDispatch: () => mockDispatch, useSelector: () => mockSelector, })); -jest.mock('../../../services', () => ({ - useWorkpadService: () => ({ - resolve: mockResolveWorkpad, - }), - usePlatformService: () => ({ - redirectLegacyUrl: mockRedirectLegacyUrl, - }), +jest.mock('../../../services/canvas_workpad_service', () => ({ + getCanvasWorkpadService: () => { + return { + resolve: mockResolveWorkpad, + }; + }, })); +spacesService!.ui.redirectLegacyUrl = mockRedirectLegacyUrl; + jest.mock('../../../state/actions/workpad', () => ({ setWorkpad: (payload: any) => ({ type: 'setWorkpad', diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts index 61908d96828fd2..c02432477a840a 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts @@ -8,7 +8,6 @@ import { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { useDispatch, useSelector } from 'react-redux'; -import { useWorkpadService, usePlatformService } from '../../../services'; import { getWorkpad } from '../../../state/selectors/workpad'; import { setWorkpad } from '../../../state/actions/workpad'; // @ts-expect-error @@ -16,7 +15,11 @@ import { setAssets } from '../../../state/actions/assets'; // @ts-expect-error import { setZoomScale } from '../../../state/actions/transient'; import { CanvasWorkpad } from '../../../../types'; -import type { ResolveWorkpadResponse } from '../../../services/workpad'; +import { + ResolveWorkpadResponse, + getCanvasWorkpadService, +} from '../../../services/canvas_workpad_service'; +import { spacesService } from '../../../services/kibana_services'; const getWorkpadLabel = () => i18n.translate('xpack.canvas.workpadResolve.redirectLabel', { @@ -32,9 +35,6 @@ export const useWorkpad = ( loadPages: boolean = true, getRedirectPath: (workpadId: string) => string ): [CanvasWorkpad | undefined, string | Error | undefined] => { - const workpadService = useWorkpadService(); - const workpadResolve = workpadService.resolve; - const platformService = usePlatformService(); const dispatch = useDispatch(); const storedWorkpad = useSelector(getWorkpad); const [error, setError] = useState(undefined); @@ -47,14 +47,12 @@ export const useWorkpad = ( const { workpad: { assets, ...workpad }, ...resolveProps - } = await workpadResolve(workpadId); - + } = await getCanvasWorkpadService().resolve(workpadId); setResolveInfo({ id: workpadId, ...resolveProps }); // If it's an alias match, we know we are going to redirect so don't even dispatch that we got the workpad if (storedWorkpad.id !== workpadId && resolveProps.outcome !== 'aliasMatch') { workpad.aliasId = resolveProps.aliasId; - dispatch(setAssets(assets)); dispatch(setWorkpad(workpad, { loadPages })); dispatch(setZoomScale(1)); @@ -63,7 +61,7 @@ export const useWorkpad = ( setError(e as Error | string); } })(); - }, [workpadId, dispatch, setError, loadPages, workpadResolve, storedWorkpad.id]); + }, [workpadId, dispatch, setError, loadPages, storedWorkpad.id]); useEffect(() => { // If the resolved info is not for the current workpad id, bail out @@ -75,16 +73,16 @@ export const useWorkpad = ( if (!resolveInfo) return; const { aliasId, outcome, aliasPurpose } = resolveInfo; - if (outcome === 'aliasMatch' && platformService.redirectLegacyUrl && aliasId) { + if (outcome === 'aliasMatch' && spacesService && aliasId) { const redirectPath = getRedirectPath(aliasId); - await platformService.redirectLegacyUrl({ + await spacesService.ui.redirectLegacyUrl({ path: `#${redirectPath}`, aliasPurpose, objectNoun: getWorkpadLabel(), }); } })(); - }, [workpadId, resolveInfo, getRedirectPath, platformService]); + }, [workpadId, resolveInfo, getRedirectPath]); return [storedWorkpad.id === workpadId ? storedWorkpad : undefined, error]; }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.test.tsx index 395cf774760fd2..3193ad3dd79e52 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.test.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.test.tsx @@ -19,12 +19,17 @@ jest.mock('react-redux', () => ({ useSelector: (selector: any) => selector(mockGetState()), })); +jest.mock('../../../services/canvas_workpad_service', () => ({ + getCanvasWorkpadService: () => { + return { + updateWorkpad: mockUpdateWorkpad, + updateAssets: mockUpdateAssets, + update: mockUpdate, + }; + }, +})); + jest.mock('../../../services', () => ({ - useWorkpadService: () => ({ - updateWorkpad: mockUpdateWorkpad, - updateAssets: mockUpdateAssets, - update: mockUpdate, - }), useNotifyService: () => ({ error: mockNotifyError, }), diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.ts index 45e21e59717ad9..f0f885c94eabc8 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.ts @@ -10,13 +10,10 @@ import { useSelector } from 'react-redux'; import { CanvasWorkpad, State } from '../../../../types'; import { getWorkpad } from '../../../state/selectors/workpad'; import { canUserWrite } from '../../../state/selectors/app'; -import { useWorkpadService, useNotifyService } from '../../../services'; import { notifyError } from '../../../lib/assets'; +import { getCanvasWorkpadService } from '../../../services/canvas_workpad_service'; export const useWorkpadPersist = () => { - const service = useWorkpadService(); - const notifyService = useNotifyService(); - // Watch for workpad state and then persist those changes const [workpad, canWrite]: [CanvasWorkpad, boolean] = useSelector((state: State) => [ getWorkpad(state), @@ -30,10 +27,12 @@ export const useWorkpadPersist = () => { useEffect(() => { if (canWrite) { if (workpadChanged) { - service.updateWorkpad(workpad.id, workpad).catch((err) => { - notifyError(err, notifyService.error); - }); + getCanvasWorkpadService() + .updateWorkpad(workpad.id, workpad) + .catch((err) => { + notifyError(err); + }); } } - }, [service, workpad, workpadChanged, canWrite, notifyService.error]); + }, [workpad, workpadChanged, canWrite]); }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx index 0dfb4dd8fbf783..bde100182788ef 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx @@ -14,7 +14,7 @@ import { getWorkpad } from '../../state/selectors/workpad'; import { useFullscreenPresentationHelper } from './hooks/use_fullscreen_presentation_helper'; import { useAutoplayHelper } from './hooks/use_autoplay_helper'; import { useRefreshHelper } from './hooks/use_refresh_helper'; -import { usePlatformService } from '../../services'; +import { coreServices, spacesService } from '../../services/kibana_services'; const getWorkpadLabel = () => i18n.translate('xpack.canvas.workpadConflict.redirectLabel', { @@ -22,7 +22,6 @@ const getWorkpadLabel = () => }); export const WorkpadPresentationHelper: FC> = ({ children }) => { - const platformService = usePlatformService(); const workpad = useSelector(getWorkpad); useFullscreenPresentationHelper(); useAutoplayHelper(); @@ -30,18 +29,18 @@ export const WorkpadPresentationHelper: FC> = ({ chil const history = useHistory(); useEffect(() => { - platformService.setBreadcrumbs([ + coreServices.chrome.setBreadcrumbs([ getBaseBreadcrumb(history), getWorkpadBreadcrumb({ name: workpad.name }), ]); - }, [workpad.name, platformService, history]); + }, [workpad.name, history]); useEffect(() => { setDocTitle(workpad.name || getUntitledWorkpadLabel()); }, [workpad.name, workpad.id]); const conflictElement = workpad.aliasId - ? platformService.getLegacyUrlConflict?.({ + ? spacesService?.ui.components.getLegacyUrlConflict({ objectNoun: getWorkpadLabel(), currentObjectId: workpad.id, otherObjectId: workpad.aliasId, diff --git a/x-pack/plugins/canvas/public/services/canvas_custom_element_service.ts b/x-pack/plugins/canvas/public/services/canvas_custom_element_service.ts new file mode 100644 index 00000000000000..1f0e13d2fbeaed --- /dev/null +++ b/x-pack/plugins/canvas/public/services/canvas_custom_element_service.ts @@ -0,0 +1,62 @@ +/* + * 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 { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib'; +import { CustomElement } from '../../types'; +import { coreServices } from './kibana_services'; + +export interface CustomElementFindResponse { + total: number; + customElements: CustomElement[]; +} + +class CanvasCustomElementService { + public apiPath = `${API_ROUTE_CUSTOM_ELEMENT}`; + + public async create(customElement: CustomElement) { + await coreServices.http.post(this.apiPath, { + body: JSON.stringify(customElement), + version: '1', + }); + } + + public async get(customElementId: string): Promise { + return await coreServices.http + .get<{ data: CustomElement }>(`${this.apiPath}/${customElementId}`, { version: '1' }) + .then(({ data: element }) => element); + } + + public async update(id: string, element: Partial) { + await coreServices.http.put(`${this.apiPath}/${id}`, { + body: JSON.stringify(element), + version: '1', + }); + } + + public async remove(id: string) { + await coreServices.http.delete(`${this.apiPath}/${id}`, { version: '1' }); + } + + public async find(searchTerm: string): Promise { + return await coreServices.http.get(`${this.apiPath}/find`, { + query: { + name: searchTerm, + perPage: 10000, + }, + version: '1', + }); + } +} + +let canvasCustomElementService: CanvasCustomElementService; + +export const getCustomElementService = () => { + if (!canvasCustomElementService) { + canvasCustomElementService = new CanvasCustomElementService(); + } + return canvasCustomElementService; +}; diff --git a/x-pack/plugins/canvas/public/services/kibana/expressions.ts b/x-pack/plugins/canvas/public/services/canvas_expressions_service.ts similarity index 65% rename from x-pack/plugins/canvas/public/services/kibana/expressions.ts rename to x-pack/plugins/canvas/public/services/canvas_expressions_service.ts index 248cd462bea909..0621c3e89416c9 100644 --- a/x-pack/plugins/canvas/public/services/kibana/expressions.ts +++ b/x-pack/plugins/canvas/public/services/canvas_expressions_service.ts @@ -4,34 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { fromExpression, getType } from '@kbn/interpreter'; import { ExpressionAstExpression, ExpressionExecutionParams, ExpressionValue, } from '@kbn/expressions-plugin/common'; +import { fromExpression, getType } from '@kbn/interpreter'; import { pluck } from 'rxjs'; -import { ExpressionsServiceStart } from '@kbn/expressions-plugin/public'; -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasFiltersService } from './filters'; -import { CanvasNotifyService } from '../notify'; +import { buildEmbeddableFilters } from '../../common/lib/build_embeddable_filters'; +import { expressionsService } from './kibana_services'; +import { getCanvasNotifyService } from './canvas_notify_service'; +import { getCanvasFiltersService } from './canvas_filters_service'; interface Options { castToRender?: boolean; } -export class ExpressionsService { - private filters: CanvasFiltersService; - private notify: CanvasNotifyService; +class ExpressionsService { + private notifyService; + private filtersService; - constructor( - private readonly expressions: ExpressionsServiceStart, - { filters, notify }: CanvasExpressionsServiceRequiredServices - ) { - this.filters = filters; - this.notify = notify; + constructor() { + this.notifyService = getCanvasNotifyService(); + this.filtersService = getCanvasFiltersService(); } async interpretAst( @@ -51,7 +46,7 @@ export class ExpressionsService { input: ExpressionValue = null, context?: ExpressionExecutionParams ): Promise { - return await this.expressions + return await expressionsService .execute(ast, input, { ...context, namespace: 'canvas' }) .getData() .pipe(pluck('result')) @@ -92,22 +87,22 @@ export class ExpressionsService { throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); } catch (err) { - this.notify.error(err); + this.notifyService.error(err); throw err; } } getRenderer(name: string) { - return this.expressions.getRenderer(name); + return expressionsService.getRenderer(name); } getFunctions() { - return this.expressions.getFunctions(); + return expressionsService.getFunctions(); } private async getFilters() { - const filtersList = this.filters.getFilters(); - const context = this.filters.getFiltersContext(); + const filtersList = this.filtersService.getFilters(); + const context = this.filtersService.getFiltersContext(); const filterExpression = filtersList.join(' | '); const filterAST = fromExpression(filterExpression); return await this.interpretAstWithContext(filterAST, null, context); @@ -122,19 +117,11 @@ export class ExpressionsService { } } -export type CanvasExpressionsService = ExpressionsService; -export interface CanvasExpressionsServiceRequiredServices { - notify: CanvasNotifyService; - filters: CanvasFiltersService; -} - -export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory< - CanvasExpressionsService, - CanvasStartDeps, - CanvasExpressionsServiceRequiredServices ->; +let canvasExpressionsService: ExpressionsService; -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( - { startPlugins }, - requiredServices -) => new ExpressionsService(startPlugins.expressions, requiredServices); +export const getCanvasExpressionService = () => { + if (!canvasExpressionsService) { + canvasExpressionsService = new ExpressionsService(); + } + return canvasExpressionsService; +}; diff --git a/x-pack/plugins/canvas/public/services/kibana/filters.ts b/x-pack/plugins/canvas/public/services/canvas_filters_service.ts similarity index 51% rename from x-pack/plugins/canvas/public/services/kibana/filters.ts rename to x-pack/plugins/canvas/public/services/canvas_filters_service.ts index 793b073aaf2313..e678e9b30963fe 100644 --- a/x-pack/plugins/canvas/public/services/kibana/filters.ts +++ b/x-pack/plugins/canvas/public/services/canvas_filters_service.ts @@ -5,25 +5,21 @@ * 2.0. */ -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; // @ts-expect-error untyped local -import { getState, getStore } from '../../state/store'; -import { State } from '../../../types'; -import { getGlobalFilters, getWorkpadVariablesAsObject } from '../../state/selectors/workpad'; -import { CanvasStartDeps } from '../../plugin'; +import { getState, getStore } from '../state/store'; +import { State } from '../../types'; +import { getGlobalFilters, getWorkpadVariablesAsObject } from '../state/selectors/workpad'; // @ts-expect-error untyped local -import { setFilter } from '../../state/actions/elements'; - -export class FiltersService { - constructor() {} +import { setFilter } from '../state/actions/filters'; +class FiltersService { getFilters(state: State = getState()) { return getGlobalFilters(state); } updateFilter(filterId: string, filterExpression: string) { const { dispatch } = getStore(); - dispatch(setFilter(filterExpression, filterId, true)); + dispatch(setFilter(filterExpression, filterId)); } getFiltersContext(state: State = getState()) { @@ -32,11 +28,11 @@ export class FiltersService { } } -export type CanvasFiltersService = FiltersService; - -export type CanvasFiltersServiceFactory = KibanaPluginServiceFactory< - CanvasFiltersService, - CanvasStartDeps ->; +let canvasFiltersService: FiltersService; -export const filtersServiceFactory: CanvasFiltersServiceFactory = () => new FiltersService(); +export const getCanvasFiltersService = () => { + if (!canvasFiltersService) { + canvasFiltersService = new FiltersService(); + } + return canvasFiltersService; +}; diff --git a/x-pack/plugins/canvas/public/services/kibana/notify.ts b/x-pack/plugins/canvas/public/services/canvas_notify_service.ts similarity index 71% rename from x-pack/plugins/canvas/public/services/kibana/notify.ts rename to x-pack/plugins/canvas/public/services/canvas_notify_service.ts index 8448a1d515fe4d..1f00190f3fff97 100644 --- a/x-pack/plugins/canvas/public/services/kibana/notify.ts +++ b/x-pack/plugins/canvas/public/services/canvas_notify_service.ts @@ -6,17 +6,10 @@ */ import { get } from 'lodash'; -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; import { ToastInputFields } from '@kbn/core/public'; -import { formatMsg } from '../../lib/format_msg'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasNotifyService } from '../notify'; - -export type CanvasNotifyServiceFactory = KibanaPluginServiceFactory< - CanvasNotifyService, - CanvasStartDeps ->; +import { formatMsg } from '../lib/format_msg'; +import { coreServices } from './kibana_services'; const getToast = (err: Error | string, opts: ToastInputFields = {}) => { const errData = (get(err, 'response') || err) as Error | string; @@ -36,8 +29,15 @@ const getToast = (err: Error | string, opts: ToastInputFields = {}) => { }; }; -export const notifyServiceFactory: CanvasNotifyServiceFactory = ({ coreStart }) => { - const toasts = coreStart.notifications.toasts; +export interface CanvasNotifyService { + error: (err: string | Error, opts?: ToastInputFields) => void; + warning: (err: string | Error, opts?: ToastInputFields) => void; + info: (err: string | Error, opts?: ToastInputFields) => void; + success: (err: string | Error, opts?: ToastInputFields) => void; +} + +export const getCanvasNotifyService = (): CanvasNotifyService => { + const toasts = coreServices.notifications.toasts; return { /* diff --git a/x-pack/plugins/canvas/public/services/canvas_workpad_service.ts b/x-pack/plugins/canvas/public/services/canvas_workpad_service.ts new file mode 100644 index 00000000000000..2672f6ef4a06e4 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/canvas_workpad_service.ts @@ -0,0 +1,200 @@ +/* + * 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 { ResolvedSimpleSavedObject, SavedObject } from '@kbn/core/public'; +import { + API_ROUTE_SHAREABLE_ZIP, + API_ROUTE_TEMPLATES, + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_ASSETS, + API_ROUTE_WORKPAD_STRUCTURES, + DEFAULT_WORKPAD_CSS, +} from '../../common/lib'; +import type { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; +import { CanvasTemplate, CanvasWorkpad } from '../../types'; +import { coreServices } from './kibana_services'; + +export type FoundWorkpads = Array>; +export type FoundWorkpad = FoundWorkpads[number]; +export interface WorkpadFindResponse { + total: number; + workpads: FoundWorkpads; +} + +export interface TemplateFindResponse { + templates: CanvasTemplate[]; +} + +export interface ResolveWorkpadResponse { + workpad: CanvasWorkpad; + outcome: ResolvedSimpleSavedObject['outcome']; + aliasId?: ResolvedSimpleSavedObject['alias_target_id']; + aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose']; +} + +/* + Remove any top level keys from the workpad which will be rejected by validation +*/ +const validKeys = [ + '@created', + '@timestamp', + 'assets', + 'colors', + 'css', + 'variables', + 'height', + 'id', + 'isWriteable', + 'name', + 'page', + 'pages', + 'width', +]; + +const sanitizeWorkpad = function (workpad: CanvasWorkpad) { + const workpadKeys = Object.keys(workpad); + + for (const key of workpadKeys) { + if (!validKeys.includes(key)) { + delete (workpad as { [key: string]: any })[key]; + } + } + + return workpad; +}; + +class CanvasWorkpadService { + private apiPath = `${API_ROUTE_WORKPAD}`; + + public async get(id: string): Promise { + const workpad = await coreServices.http.get(`${this.apiPath}/${id}`, { version: '1' }); + + return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad }; + } + + public async export(id: string) { + const workpad = await coreServices.http.get>( + `${this.apiPath}/export/${id}`, + { version: '1' } + ); + const { attributes } = workpad; + + return { + ...workpad, + attributes: { + ...attributes, + css: attributes.css ?? DEFAULT_WORKPAD_CSS, + variables: attributes.variables ?? [], + }, + }; + } + + public async resolve(id: string): Promise { + const { workpad, ...resolveProps } = await coreServices.http.get( + `${this.apiPath}/resolve/${id}`, + { version: '1' } + ); + + return { + ...resolveProps, + workpad: { + // @ts-ignore: Shimming legacy workpads that might not have CSS + css: DEFAULT_WORKPAD_CSS, + // @ts-ignore: Shimming legacy workpads that might not have variables + variables: [], + ...workpad, + }, + }; + } + + public async create(workpad: CanvasWorkpad): Promise { + return coreServices.http.post(this.apiPath, { + body: JSON.stringify({ + ...sanitizeWorkpad({ ...workpad }), + assets: workpad.assets || {}, + variables: workpad.variables || [], + }), + version: '1', + }); + } + + public async import(workpad: CanvasWorkpad): Promise { + return coreServices.http.post(`${this.apiPath}/import`, { + body: JSON.stringify({ + ...sanitizeWorkpad({ ...workpad }), + assets: workpad.assets || {}, + variables: workpad.variables || [], + }), + version: '1', + }); + } + + public async createFromTemplate(templateId: string): Promise { + return coreServices.http.post(this.apiPath, { + body: JSON.stringify({ templateId }), + version: '1', + }); + } + + public async findTemplates(): Promise { + return coreServices.http.get(API_ROUTE_TEMPLATES, { version: '1' }); + } + + public async find(searchTerm: string): Promise { + // TODO: this shouldn't be necessary. Check for usage. + const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0; + + return coreServices.http.get(`${this.apiPath}/find`, { + query: { + perPage: 10000, + name: validSearchTerm ? searchTerm : '', + }, + version: '1', + }); + } + + public async remove(id: string) { + coreServices.http.delete(`${this.apiPath}/${id}`, { version: '1' }); + } + + public async update(id: string, workpad: CanvasWorkpad) { + coreServices.http.put(`${this.apiPath}/${id}`, { + body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }), + version: '1', + }); + } + + public async updateWorkpad(id: string, workpad: CanvasWorkpad) { + coreServices.http.put(`${API_ROUTE_WORKPAD_STRUCTURES}/${id}`, { + body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }), + version: '1', + }); + } + + public async updateAssets(id: string, assets: CanvasWorkpad['assets']) { + coreServices.http.put(`${API_ROUTE_WORKPAD_ASSETS}/${id}`, { + body: JSON.stringify(assets), + version: '1', + }); + } + + public async getRuntimeZip(workpad: CanvasRenderedWorkpad): Promise { + return coreServices.http.post(API_ROUTE_SHAREABLE_ZIP, { + body: JSON.stringify(workpad), + version: '1', + }); + } +} + +let canvasWorkpadService: CanvasWorkpadService; + +export const getCanvasWorkpadService: () => CanvasWorkpadService = () => { + if (!canvasWorkpadService) { + canvasWorkpadService = new CanvasWorkpadService(); + } + return canvasWorkpadService; +}; diff --git a/x-pack/plugins/canvas/public/services/custom_element.ts b/x-pack/plugins/canvas/public/services/custom_element.ts deleted file mode 100644 index 675a5a2f23c015..00000000000000 --- a/x-pack/plugins/canvas/public/services/custom_element.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CustomElement } from '../../types'; - -export interface CustomElementFindResponse { - total: number; - customElements: CustomElement[]; -} - -export interface CanvasCustomElementService { - create: (customElement: CustomElement) => Promise; - get: (customElementId: string) => Promise; - update: (id: string, element: Partial) => Promise; - remove: (id: string) => Promise; - find: (searchTerm: string) => Promise; -} diff --git a/x-pack/plugins/canvas/public/services/data_views.ts b/x-pack/plugins/canvas/public/services/data_views.ts deleted file mode 100644 index 86faa87bfaa597..00000000000000 --- a/x-pack/plugins/canvas/public/services/data_views.ts +++ /dev/null @@ -1,14 +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 { DataView } from '@kbn/data-views-plugin/common'; - -export interface CanvasDataViewsService { - getFields: (index: string) => Promise; - getDataViews: () => Promise>>; - getDefaultDataView: () => Promise | undefined>; -} diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/embeddables.ts deleted file mode 100644 index b8f3d7d14b5e51..00000000000000 --- a/x-pack/plugins/canvas/public/services/embeddables.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EmbeddableFactory, - type EmbeddableStateTransfer, - ReactEmbeddableSavedObject, -} from '@kbn/embeddable-plugin/public'; -import { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common'; - -export interface CanvasEmbeddablesService { - reactEmbeddableRegistryHasKey: (key: string) => boolean; - getReactEmbeddableSavedObjects: < - TSavedObjectAttributes extends FinderAttributes - >() => IterableIterator<[string, ReactEmbeddableSavedObject]>; - getEmbeddableFactories: () => IterableIterator; - getStateTransfer: () => EmbeddableStateTransfer; -} diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts deleted file mode 100644 index 456a1314bdfffc..00000000000000 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ /dev/null @@ -1,8 +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. - */ - -export type { CanvasExpressionsService } from './kibana/expressions'; diff --git a/x-pack/plugins/canvas/public/services/filters.ts b/x-pack/plugins/canvas/public/services/filters.ts deleted file mode 100644 index 1ced3d15f6e100..00000000000000 --- a/x-pack/plugins/canvas/public/services/filters.ts +++ /dev/null @@ -1,8 +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. - */ - -export type { CanvasFiltersService } from './kibana/filters'; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index dd035fcd3b1ad0..92b5f0ef3936a7 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -6,55 +6,11 @@ */ export * from './legacy'; +import { useMemo } from 'react'; -import { PluginServices } from '@kbn/presentation-util-plugin/public'; +import { getCanvasNotifyService } from './canvas_notify_service'; -import { CanvasCustomElementService } from './custom_element'; -import { CanvasDataViewsService } from './data_views'; -import { CanvasEmbeddablesService } from './embeddables'; -import { CanvasExpressionsService } from './expressions'; -import { CanvasFiltersService } from './filters'; -import { CanvasLabsService } from './labs'; -import { CanvasNavLinkService } from './nav_link'; -import { CanvasNotifyService } from './notify'; -import { CanvasPlatformService } from './platform'; -import { CanvasReportingService } from './reporting'; -import { CanvasVisualizationsService } from './visualizations'; -import { CanvasWorkpadService } from './workpad'; -import { CanvasUiActionsService } from './ui_actions'; - -export interface CanvasPluginServices { - customElement: CanvasCustomElementService; - dataViews: CanvasDataViewsService; - embeddables: CanvasEmbeddablesService; - expressions: CanvasExpressionsService; - filters: CanvasFiltersService; - labs: CanvasLabsService; - navLink: CanvasNavLinkService; - notify: CanvasNotifyService; - platform: CanvasPlatformService; - reporting: CanvasReportingService; - visualizations: CanvasVisualizationsService; - workpad: CanvasWorkpadService; - uiActions: CanvasUiActionsService; -} - -export const pluginServices = new PluginServices(); - -export const useCustomElementService = () => - (() => pluginServices.getHooks().customElement.useService())(); -export const useDataViewsService = () => (() => pluginServices.getHooks().dataViews.useService())(); -export const useEmbeddablesService = () => - (() => pluginServices.getHooks().embeddables.useService())(); -export const useExpressionsService = () => - (() => pluginServices.getHooks().expressions.useService())(); -export const useFiltersService = () => (() => pluginServices.getHooks().filters.useService())(); -export const useLabsService = () => (() => pluginServices.getHooks().labs.useService())(); -export const useNavLinkService = () => (() => pluginServices.getHooks().navLink.useService())(); -export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); -export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); -export const useReportingService = () => (() => pluginServices.getHooks().reporting.useService())(); -export const useVisualizationsService = () => - (() => pluginServices.getHooks().visualizations.useService())(); -export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())(); -export const useUiActionsService = () => (() => pluginServices.getHooks().uiActions.useService())(); +export const useNotifyService = () => { + const canvasNotifyService = useMemo(() => getCanvasNotifyService(), []); + return canvasNotifyService; +}; diff --git a/x-pack/plugins/canvas/public/services/kibana/custom_element.ts b/x-pack/plugins/canvas/public/services/kibana/custom_element.ts deleted file mode 100644 index a548ccdc23b2f4..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/custom_element.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { API_ROUTE_CUSTOM_ELEMENT } from '../../../common/lib/constants'; -import { CustomElement } from '../../../types'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasCustomElementService } from '../custom_element'; - -export type CanvasCustomElementServiceFactory = KibanaPluginServiceFactory< - CanvasCustomElementService, - CanvasStartDeps ->; - -export const customElementServiceFactory: CanvasCustomElementServiceFactory = ({ coreStart }) => { - const { http } = coreStart; - const apiPath = `${API_ROUTE_CUSTOM_ELEMENT}`; - - return { - create: (customElement) => - http.post(apiPath, { body: JSON.stringify(customElement), version: '1' }), - get: (customElementId) => - http - .get<{ data: CustomElement }>(`${apiPath}/${customElementId}`, { version: '1' }) - .then(({ data: element }) => element), - update: (id, element) => - http.put(`${apiPath}/${id}`, { body: JSON.stringify(element), version: '1' }), - remove: (id) => http.delete(`${apiPath}/${id}`, { version: '1' }), - find: async (name) => { - return http.get(`${apiPath}/find`, { - query: { - name, - perPage: 10000, - }, - version: '1', - }); - }, - }; -}; diff --git a/x-pack/plugins/canvas/public/services/kibana/data_views.ts b/x-pack/plugins/canvas/public/services/kibana/data_views.ts deleted file mode 100644 index 27ec0bb1c5fc60..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/data_views.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataView } from '@kbn/data-views-plugin/public'; -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ErrorStrings } from '../../../i18n'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasDataViewsService } from '../data_views'; -import { CanvasNotifyService } from '../notify'; - -const { esService: strings } = ErrorStrings; - -export type DataViewsServiceFactory = KibanaPluginServiceFactory< - CanvasDataViewsService, - CanvasStartDeps, - { - notify: CanvasNotifyService; - } ->; - -export const dataViewsServiceFactory: DataViewsServiceFactory = ({ startPlugins }, { notify }) => ({ - getDataViews: async () => { - try { - const dataViews = await startPlugins.dataViews.getIdsWithTitle(); - return dataViews.map(({ id, name, title }) => ({ id, name, title } as DataView)); - } catch (e) { - notify.error(e, { title: strings.getIndicesFetchErrorMessage() }); - } - - return []; - }, - getFields: async (dataViewTitle: string) => { - const dataView = await startPlugins.dataViews.create({ title: dataViewTitle }); - - return dataView.fields - .filter((field) => !field.name.startsWith('_')) - .map((field) => field.name); - }, - getDefaultDataView: async () => { - const dataView = await startPlugins.dataViews.getDefaultDataView(); - - return dataView - ? { id: dataView.id, name: dataView.name, title: dataView.getIndexPattern() } - : undefined; - }, -}); diff --git a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts deleted file mode 100644 index 20f6f3feec8a4c..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasEmbeddablesService } from '../embeddables'; - -export type EmbeddablesServiceFactory = KibanaPluginServiceFactory< - CanvasEmbeddablesService, - CanvasStartDeps ->; - -export const embeddablesServiceFactory: EmbeddablesServiceFactory = ({ startPlugins }) => ({ - reactEmbeddableRegistryHasKey: startPlugins.embeddable.reactEmbeddableRegistryHasKey, - getReactEmbeddableSavedObjects: startPlugins.embeddable.getReactEmbeddableSavedObjects, - getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories, - getStateTransfer: startPlugins.embeddable.getStateTransfer, -}); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts deleted file mode 100644 index cab569133f70f5..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ /dev/null @@ -1,66 +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 { - PluginServiceProviders, - PluginServiceProvider, - PluginServiceRegistry, - KibanaPluginServiceParams, -} from '@kbn/presentation-util-plugin/public'; - -import { CanvasPluginServices } from '..'; -import { CanvasStartDeps } from '../../plugin'; -import { customElementServiceFactory } from './custom_element'; -import { dataViewsServiceFactory } from './data_views'; -import { embeddablesServiceFactory } from './embeddables'; -import { expressionsServiceFactory } from './expressions'; -import { labsServiceFactory } from './labs'; -import { navLinkServiceFactory } from './nav_link'; -import { notifyServiceFactory } from './notify'; -import { platformServiceFactory } from './platform'; -import { reportingServiceFactory } from './reporting'; -import { visualizationsServiceFactory } from './visualizations'; -import { workpadServiceFactory } from './workpad'; -import { filtersServiceFactory } from './filters'; -import { uiActionsServiceFactory } from './ui_actions'; - -export { customElementServiceFactory } from './custom_element'; -export { dataViewsServiceFactory } from './data_views'; -export { embeddablesServiceFactory } from './embeddables'; -export { expressionsServiceFactory } from './expressions'; -export { filtersServiceFactory } from './filters'; -export { labsServiceFactory } from './labs'; -export { notifyServiceFactory } from './notify'; -export { platformServiceFactory } from './platform'; -export { reportingServiceFactory } from './reporting'; -export { visualizationsServiceFactory } from './visualizations'; -export { workpadServiceFactory } from './workpad'; -export { uiActionsServiceFactory } from './ui_actions'; - -export const pluginServiceProviders: PluginServiceProviders< - CanvasPluginServices, - KibanaPluginServiceParams -> = { - customElement: new PluginServiceProvider(customElementServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory, ['notify']), - embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), - filters: new PluginServiceProvider(filtersServiceFactory), - labs: new PluginServiceProvider(labsServiceFactory), - navLink: new PluginServiceProvider(navLinkServiceFactory), - notify: new PluginServiceProvider(notifyServiceFactory), - platform: new PluginServiceProvider(platformServiceFactory), - reporting: new PluginServiceProvider(reportingServiceFactory), - visualizations: new PluginServiceProvider(visualizationsServiceFactory), - workpad: new PluginServiceProvider(workpadServiceFactory), - uiActions: new PluginServiceProvider(uiActionsServiceFactory), -}; - -export const pluginServiceRegistry = new PluginServiceRegistry< - CanvasPluginServices, - KibanaPluginServiceParams ->(pluginServiceProviders); diff --git a/x-pack/plugins/canvas/public/services/kibana/labs.ts b/x-pack/plugins/canvas/public/services/kibana/labs.ts deleted file mode 100644 index 69613ae67fd85b..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/labs.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 { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { projectIDs } from '@kbn/presentation-util-plugin/common'; -import { UI_SETTINGS } from '../../../common'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasLabsService } from '../labs'; - -export type CanvasLabsServiceFactory = KibanaPluginServiceFactory< - CanvasLabsService, - CanvasStartDeps ->; - -export const labsServiceFactory: CanvasLabsServiceFactory = ({ startPlugins, coreStart }) => ({ - projectIDs, - isLabsEnabled: () => coreStart.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), - ...startPlugins.presentationUtil.labsService, -}); diff --git a/x-pack/plugins/canvas/public/services/kibana/nav_link.ts b/x-pack/plugins/canvas/public/services/kibana/nav_link.ts deleted file mode 100644 index 8470c688f5b2b4..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/nav_link.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { SESSIONSTORAGE_LASTPATH } from '../../../common/lib/constants'; -import { getSessionStorage } from '../../lib/storage'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasNavLinkService } from '../nav_link'; - -export type CanvasNavLinkServiceFactory = KibanaPluginServiceFactory< - CanvasNavLinkService, - CanvasStartDeps ->; - -export const navLinkServiceFactory: CanvasNavLinkServiceFactory = ({ coreStart, appUpdater }) => ({ - updatePath: (path: string) => { - appUpdater?.next(() => ({ - defaultPath: `${path}`, - })); - - getSessionStorage().set(`${SESSIONSTORAGE_LASTPATH}:${coreStart.http.basePath.get()}`, path); - }, -}); diff --git a/x-pack/plugins/canvas/public/services/kibana/platform.ts b/x-pack/plugins/canvas/public/services/kibana/platform.ts deleted file mode 100644 index 1518280ab0688f..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/platform.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { CanvasStartDeps } from '../../plugin'; -import { CanvasPlatformService } from '../platform'; - -export type CanvaPlatformServiceFactory = KibanaPluginServiceFactory< - CanvasPlatformService, - CanvasStartDeps ->; - -export const platformServiceFactory: CanvaPlatformServiceFactory = ({ - coreStart, - initContext, - startPlugins, -}) => { - if (!initContext) { - throw new Error('Canvas platform service requires init context'); - } - - return { - getBasePath: coreStart.http.basePath.get, - getBasePathInterface: () => coreStart.http.basePath, - getElasticWebsiteUrl: () => coreStart.docLinks.ELASTIC_WEBSITE_URL, - getDocLinkVersion: () => coreStart.docLinks.DOC_LINK_VERSION, - getKibanaVersion: () => initContext.env.packageInfo.version, - // TODO: is there a better type for this? The capabilities type allows for a Record, - // though we don't do this. So this cast may be the best option. - getHasWriteAccess: () => coreStart.application.capabilities.canvas.save as boolean, - getUISetting: coreStart.uiSettings.get.bind(coreStart.uiSettings), - hasHeaderBanner$: coreStart.chrome.hasHeaderBanner$, - setBreadcrumbs: coreStart.chrome.setBreadcrumbs, - setRecentlyAccessed: coreStart.chrome.recentlyAccessed.add, - setFullscreen: coreStart.chrome.setIsVisible, - redirectLegacyUrl: startPlugins.spaces?.ui.redirectLegacyUrl, - getLegacyUrlConflict: startPlugins.spaces?.ui.components.getLegacyUrlConflict, - getUISettings: () => coreStart.uiSettings, - getHttp: () => coreStart.http, - getContentManagement: () => startPlugins.contentManagement, - }; -}; diff --git a/x-pack/plugins/canvas/public/services/kibana/reporting.ts b/x-pack/plugins/canvas/public/services/kibana/reporting.ts deleted file mode 100644 index 56b9b91e45fc69..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/reporting.ts +++ /dev/null @@ -1,45 +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 { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { CanvasStartDeps } from '../../plugin'; -import { CanvasReportingService } from '../reporting'; - -export type CanvasReportingServiceFactory = KibanaPluginServiceFactory< - CanvasReportingService, - CanvasStartDeps ->; - -export const reportingServiceFactory: CanvasReportingServiceFactory = ({ - startPlugins, - coreStart, -}) => { - const { reporting } = startPlugins; - - const reportingEnabled = () => ({ - getReportingPanelPDFComponent: () => reporting?.components.ReportingPanelPDFV2 || null, - }); - const reportingDisabled = () => ({ getReportingPanelPDFComponent: () => null }); - - if (!reporting) { - // Reporting is not enabled - return reportingDisabled(); - } - - if (reporting.usesUiCapabilities()) { - if (coreStart.application.capabilities.canvas?.generatePdf === true) { - // Canvas has declared Reporting as a subfeature with the `generatePdf` UI Capability - return reportingEnabled(); - } else { - return reportingDisabled(); - } - } - - // Legacy/Deprecated: Reporting is enabled as an Elasticsearch feature - return reportingEnabled(); -}; diff --git a/x-pack/plugins/canvas/public/services/kibana/ui_actions.ts b/x-pack/plugins/canvas/public/services/kibana/ui_actions.ts deleted file mode 100644 index bb621206082d16..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/ui_actions.ts +++ /dev/null @@ -1,19 +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 { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasUiActionsService } from '../ui_actions'; - -export type UiActionsServiceFactory = KibanaPluginServiceFactory< - CanvasUiActionsService, - CanvasStartDeps ->; - -export const uiActionsServiceFactory: UiActionsServiceFactory = ({ startPlugins }) => ({ - getTriggerCompatibleActions: startPlugins.uiActions.getTriggerCompatibleActions, -}); diff --git a/x-pack/plugins/canvas/public/services/kibana/visualizations.ts b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts deleted file mode 100644 index 6f1728953ee8eb..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/visualizations.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasStartDeps } from '../../plugin'; -import { CanvasVisualizationsService } from '../visualizations'; - -export type VisualizationsServiceFactory = KibanaPluginServiceFactory< - CanvasVisualizationsService, - CanvasStartDeps ->; - -export const visualizationsServiceFactory: VisualizationsServiceFactory = ({ startPlugins }) => ({ - showNewVisModal: startPlugins.visualizations.showNewVisModal, - getByGroup: startPlugins.visualizations.getByGroup, - getAliases: startPlugins.visualizations.getAliases, -}); diff --git a/x-pack/plugins/canvas/public/services/kibana/workpad.ts b/x-pack/plugins/canvas/public/services/kibana/workpad.ts deleted file mode 100644 index 3b70244440cc85..00000000000000 --- a/x-pack/plugins/canvas/public/services/kibana/workpad.ts +++ /dev/null @@ -1,170 +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 { SavedObject } from '@kbn/core/public'; -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { CanvasStartDeps } from '../../plugin'; -import { CanvasWorkpadService, ResolveWorkpadResponse } from '../workpad'; - -import { - API_ROUTE_WORKPAD, - DEFAULT_WORKPAD_CSS, - API_ROUTE_TEMPLATES, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, - API_ROUTE_SHAREABLE_ZIP, -} from '../../../common/lib/constants'; -import { CanvasWorkpad } from '../../../types'; - -export type CanvasWorkpadServiceFactory = KibanaPluginServiceFactory< - CanvasWorkpadService, - CanvasStartDeps ->; - -/* - Remove any top level keys from the workpad which will be rejected by validation -*/ -const validKeys = [ - '@created', - '@timestamp', - 'assets', - 'colors', - 'css', - 'variables', - 'height', - 'id', - 'isWriteable', - 'name', - 'page', - 'pages', - 'width', -]; - -const sanitizeWorkpad = function (workpad: CanvasWorkpad) { - const workpadKeys = Object.keys(workpad); - - for (const key of workpadKeys) { - if (!validKeys.includes(key)) { - delete (workpad as { [key: string]: any })[key]; - } - } - - return workpad; -}; - -export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, startPlugins }) => { - const getApiPath = function () { - return `${API_ROUTE_WORKPAD}`; - }; - - return { - get: async (id: string) => { - const workpad = await coreStart.http.get(`${getApiPath()}/${id}`, { version: '1' }); - - return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad }; - }, - export: async (id: string) => { - const workpad = await coreStart.http.get>( - `${getApiPath()}/export/${id}`, - { version: '1' } - ); - const { attributes } = workpad; - - return { - ...workpad, - attributes: { - ...attributes, - css: attributes.css ?? DEFAULT_WORKPAD_CSS, - variables: attributes.variables ?? [], - }, - }; - }, - resolve: async (id: string) => { - const { workpad, ...resolveProps } = await coreStart.http.get( - `${getApiPath()}/resolve/${id}`, - { version: '1' } - ); - - return { - ...resolveProps, - workpad: { - // @ts-ignore: Shimming legacy workpads that might not have CSS - css: DEFAULT_WORKPAD_CSS, - // @ts-ignore: Shimming legacy workpads that might not have variables - variables: [], - ...workpad, - }, - }; - }, - create: (workpad: CanvasWorkpad) => { - return coreStart.http.post(getApiPath(), { - body: JSON.stringify({ - ...sanitizeWorkpad({ ...workpad }), - assets: workpad.assets || {}, - variables: workpad.variables || [], - }), - version: '1', - }); - }, - import: (workpad: CanvasWorkpad) => - coreStart.http.post(`${getApiPath()}/import`, { - body: JSON.stringify({ - ...sanitizeWorkpad({ ...workpad }), - assets: workpad.assets || {}, - variables: workpad.variables || [], - }), - version: '1', - }), - createFromTemplate: (templateId: string) => { - return coreStart.http.post(getApiPath(), { - body: JSON.stringify({ templateId }), - version: '1', - }); - }, - findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES, { version: '1' }), - find: (searchTerm: string) => { - // TODO: this shouldn't be necessary. Check for usage. - const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0; - - return coreStart.http.get(`${getApiPath()}/find`, { - query: { - perPage: 10000, - name: validSearchTerm ? searchTerm : '', - }, - version: '1', - }); - }, - remove: (id: string) => { - return coreStart.http.delete(`${getApiPath()}/${id}`, { version: '1' }); - }, - update: (id, workpad) => { - return coreStart.http.put(`${getApiPath()}/${id}`, { - body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }), - version: '1', - }); - }, - updateWorkpad: (id, workpad) => { - return coreStart.http.put(`${API_ROUTE_WORKPAD_STRUCTURES}/${id}`, { - body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }), - version: '1', - }); - }, - updateAssets: (id, assets) => { - return coreStart.http.put(`${API_ROUTE_WORKPAD_ASSETS}/${id}`, { - body: JSON.stringify(assets), - version: '1', - }); - }, - getRuntimeZip: (workpad) => { - return coreStart.http.post(API_ROUTE_SHAREABLE_ZIP, { - body: JSON.stringify(workpad), - version: '1', - }); - }, - }; -}; diff --git a/x-pack/plugins/canvas/public/services/kibana_services.ts b/x-pack/plugins/canvas/public/services/kibana_services.ts new file mode 100644 index 00000000000000..2b966e698a874d --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana_services.ts @@ -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 { BehaviorSubject } from 'rxjs'; + +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public/plugin'; +import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; +import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; +import type { ReportingStart } from '@kbn/reporting-plugin/public'; +import type { SpacesApi } from '@kbn/spaces-plugin/public'; +import type { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin'; +import type { VisualizationsStart } from '@kbn/visualizations-plugin/public'; + +import type { CanvasStartDeps } from '../plugin'; + +export let kibanaVersion: string; + +export let coreServices: CoreStart; +export let contentManagementService: ContentManagementPublicStart; +export let dataService: DataPublicPluginStart; +export let dataViewsService: DataViewsPublicPluginStart; +export let embeddableService: EmbeddableStart; +export let expressionsService: ExpressionsStart; +export let presentationUtilService: PresentationUtilPluginStart; +export let reportingService: ReportingStart | undefined; +export let spacesService: SpacesApi | undefined; +export let uiActionsService: UiActionsPublicStart; +export let visualizationsService: VisualizationsStart; + +const servicesReady$ = new BehaviorSubject(false); + +export const setKibanaServices = ( + kibanaCore: CoreStart, + deps: CanvasStartDeps, + initContext: PluginInitializerContext +) => { + kibanaVersion = initContext.env.packageInfo.version; + + coreServices = kibanaCore; + contentManagementService = deps.contentManagement; + dataService = deps.data; + dataViewsService = deps.dataViews; + embeddableService = deps.embeddable; + expressionsService = deps.expressions; + presentationUtilService = deps.presentationUtil; + reportingService = Boolean( + deps.reporting?.usesUiCapabilities() && !kibanaCore.application.capabilities.canvas?.generatePdf + ) + ? undefined + : deps.reporting; + spacesService = deps.spaces; + uiActionsService = deps.uiActions; + visualizationsService = deps.visualizations; + + servicesReady$.next(true); +}; + +export const untilPluginStartServicesReady = () => { + if (servicesReady$.value) return Promise.resolve(); + return new Promise((resolve) => { + const subscription = servicesReady$.subscribe((isInitialized) => { + if (isInitialized) { + subscription.unsubscribe(); + resolve(); + } + }); + }); +}; diff --git a/x-pack/plugins/canvas/public/services/labs.ts b/x-pack/plugins/canvas/public/services/labs.ts deleted file mode 100644 index 0f20e25f31d60f..00000000000000 --- a/x-pack/plugins/canvas/public/services/labs.ts +++ /dev/null @@ -1,14 +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 { PresentationLabsService } from '@kbn/presentation-util-plugin/public'; -import { projectIDs } from '@kbn/presentation-util-plugin/common'; - -export interface CanvasLabsService extends PresentationLabsService { - projectIDs: typeof projectIDs; - isLabsEnabled: () => boolean; -} diff --git a/x-pack/plugins/canvas/public/services/legacy/reporting.ts b/x-pack/plugins/canvas/public/services/legacy/reporting.ts deleted file mode 100644 index 411a892baed295..00000000000000 --- a/x-pack/plugins/canvas/public/services/legacy/reporting.ts +++ /dev/null @@ -1,42 +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 { ReportingStart } from '@kbn/reporting-plugin/public'; -import { CanvasServiceFactory } from '.'; - -export interface ReportingService { - start?: ReportingStart; -} - -export const reportingServiceFactory: CanvasServiceFactory = ( - _coreSetup, - coreStart, - _setupPlugins, - startPlugins -): ReportingService => { - const { reporting } = startPlugins; - - const reportingEnabled = () => ({ start: reporting }); - const reportingDisabled = () => ({ start: undefined }); - - if (!reporting) { - // Reporting is not enabled - return reportingDisabled(); - } - - if (reporting.usesUiCapabilities()) { - if (coreStart.application.capabilities.canvas?.generatePdf === true) { - // Canvas has declared Reporting as a subfeature with the `generatePdf` UI Capability - return reportingEnabled(); - } else { - return reportingDisabled(); - } - } - - // Legacy/Deprecated: Reporting is enabled as an Elasticsearch feature - return reportingEnabled(); -}; diff --git a/x-pack/plugins/canvas/public/services/mocks.ts b/x-pack/plugins/canvas/public/services/mocks.ts new file mode 100644 index 00000000000000..c35d8834253be4 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/mocks.ts @@ -0,0 +1,125 @@ +/* + * 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 moment from 'moment'; + +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; +import { CoreStart } from '@kbn/core/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks'; +import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; +import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks'; +import { reportingPluginMock } from '@kbn/reporting-plugin/public/mocks'; +import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; +import { visualizationsPluginMock } from '@kbn/visualizations-plugin/public/mocks'; + +import { setKibanaServices } from './kibana_services'; +import { getId } from '../lib/get_id'; +// @ts-expect-error +import { getDefaultWorkpad } from '../state/defaults'; + +const setDefaultPresentationUtilCapabilities = (core: CoreStart) => { + core.application.capabilities = { + ...core.application.capabilities, + dashboard: { + show: true, + createNew: true, + }, + visualize: { + save: true, + }, + advancedSettings: { + save: true, + }, + }; +}; + +export const setStubKibanaServices = () => { + const core: CoreStart = coreMock.createStart(); + + setDefaultPresentationUtilCapabilities(core); + setKibanaServices( + core, + { + charts: chartPluginMock.createStartContract(), + contentManagement: contentManagementMock.createStartContract(), + data: dataPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), + expressions: expressionsPluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), + presentationUtil: presentationUtilPluginMock.createStartContract(), + reporting: reportingPluginMock.createStartContract(), + spaces: spacesPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), + visualizations: visualizationsPluginMock.createStartContract(), + }, + coreMock.createPluginInitializerContext() + ); +}; + +const TIMEOUT = 500; + +export const getSomeWorkpads = (count = 3, useStaticData = false) => { + if (useStaticData) { + const DAY = 86400000; + const JAN_1_2000 = 946684800000; + + const workpads = []; + for (let i = 0; i < count; i++) { + workpads[i] = { + ...getDefaultWorkpad(), + name: `Workpad ${i}`, + id: `workpad-${i}`, + '@created': moment(JAN_1_2000 + DAY * i).toDate(), + '@timestamp': moment(JAN_1_2000 + DAY * (i + 1)).toDate(), + }; + } + return workpads; + } else { + return Array.from({ length: count }, () => ({ + '@created': getRandomDate( + moment().subtract(3, 'days').toDate(), + moment().subtract(10, 'days').toDate() + ), + '@timestamp': getRandomDate(), + id: getId('workpad'), + name: getRandomName(), + })); + } +}; + +export const findSomeWorkpads = + (count = 3, useStaticData = false, timeout = TIMEOUT) => + (_term: string) => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => ({ + total: count, + workpads: getSomeWorkpads(count, useStaticData), + })); + }; + +const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time)); + +const getRandomName = () => { + const lorem = + 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split( + ' ' + ); + return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' '); +}; + +const getRandomDate = ( + start: Date = moment().toDate(), + end: Date = moment().subtract(7, 'days').toDate() +) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString(); diff --git a/x-pack/plugins/canvas/public/services/notify.ts b/x-pack/plugins/canvas/public/services/notify.ts deleted file mode 100644 index 83e7b4e71ee725..00000000000000 --- a/x-pack/plugins/canvas/public/services/notify.ts +++ /dev/null @@ -1,15 +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 { ToastInputFields } from '@kbn/core/public'; - -export interface CanvasNotifyService { - error: (err: string | Error, opts?: ToastInputFields) => void; - warning: (err: string | Error, opts?: ToastInputFields) => void; - info: (err: string | Error, opts?: ToastInputFields) => void; - success: (err: string | Error, opts?: ToastInputFields) => void; -} diff --git a/x-pack/plugins/canvas/public/services/platform.ts b/x-pack/plugins/canvas/public/services/platform.ts deleted file mode 100644 index 555f8643fff9bd..00000000000000 --- a/x-pack/plugins/canvas/public/services/platform.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Observable } from 'rxjs'; -import { - IUiSettingsClient, - ChromeBreadcrumb, - IBasePath, - ChromeStart, - HttpStart, -} from '@kbn/core/public'; - -import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; - -export interface CanvasPlatformService { - getBasePath: () => string; - getBasePathInterface: () => IBasePath; - getDocLinkVersion: () => string; - getElasticWebsiteUrl: () => string; - getKibanaVersion: () => string; - getHasWriteAccess: () => boolean; - getUISetting: (key: string, defaultValue?: any) => any; - hasHeaderBanner$: () => Observable; - setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; - setRecentlyAccessed: (link: string, label: string, id: string) => void; - setFullscreen: ChromeStart['setIsVisible']; - redirectLegacyUrl?: SpacesPluginStart['ui']['redirectLegacyUrl']; - getLegacyUrlConflict?: SpacesPluginStart['ui']['components']['getLegacyUrlConflict']; - getUISettings: () => IUiSettingsClient; - getHttp: () => HttpStart; - getContentManagement: () => ContentManagementPublicStart; -} diff --git a/x-pack/plugins/canvas/public/services/reporting.ts b/x-pack/plugins/canvas/public/services/reporting.ts deleted file mode 100644 index 71d90421eafa8a..00000000000000 --- a/x-pack/plugins/canvas/public/services/reporting.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ReportingStart } from '@kbn/reporting-plugin/public'; - -type ReportingPanelPDFComponent = ReportingStart['components']['ReportingPanelPDFV2']; -export interface CanvasReportingService { - getReportingPanelPDFComponent: () => ReportingPanelPDFComponent | null; -} diff --git a/x-pack/plugins/canvas/public/services/storybook/index.ts b/x-pack/plugins/canvas/public/services/storybook/index.ts deleted file mode 100644 index 52cad460662b2d..00000000000000 --- a/x-pack/plugins/canvas/public/services/storybook/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - PluginServiceProviders, - PluginServiceProvider, -} from '@kbn/presentation-util-plugin/public'; - -import { CanvasPluginServices } from '..'; -import { pluginServiceProviders as stubProviders } from '../stubs'; -import { workpadServiceFactory } from './workpad'; -import { notifyServiceFactory } from './notify'; - -export interface StorybookParams { - hasTemplates?: boolean; - useStaticData?: boolean; - workpadCount?: number; -} - -export const pluginServiceProviders: PluginServiceProviders = - { - ...stubProviders, - workpad: new PluginServiceProvider(workpadServiceFactory), - notify: new PluginServiceProvider(notifyServiceFactory), - }; - -export const argTypes = { - hasTemplates: { - name: 'Has templates?', - type: { - name: 'boolean', - }, - defaultValue: true, - control: { - type: 'boolean', - }, - }, - useStaticData: { - name: 'Use static data?', - type: { - name: 'boolean', - }, - defaultValue: false, - control: { - type: 'boolean', - }, - }, - workpadCount: { - name: 'Number of workpads', - type: { name: 'number' }, - defaultValue: 5, - control: { - type: 'range', - }, - }, -}; diff --git a/x-pack/plugins/canvas/public/services/storybook/notify.ts b/x-pack/plugins/canvas/public/services/storybook/notify.ts deleted file mode 100644 index f6f97c42a17416..00000000000000 --- a/x-pack/plugins/canvas/public/services/storybook/notify.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { action } from '@storybook/addon-actions'; - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { StorybookParams } from '.'; -import { CanvasNotifyService } from '../notify'; - -type CanvasNotifyServiceFactory = PluginServiceFactory; - -export const notifyServiceFactory: CanvasNotifyServiceFactory = () => ({ - success: (message) => action(`success: ${message}`)(), - error: (message) => action(`error: ${message}`)(), - info: (message) => action(`info: ${message}`)(), - warning: (message) => action(`warning: ${message}`)(), -}); diff --git a/x-pack/plugins/canvas/public/services/storybook/workpad.ts b/x-pack/plugins/canvas/public/services/storybook/workpad.ts deleted file mode 100644 index 9f40ccccafb3d5..00000000000000 --- a/x-pack/plugins/canvas/public/services/storybook/workpad.ts +++ /dev/null @@ -1,122 +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 moment from 'moment'; - -import { action } from '@storybook/addon-actions'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { getId } from '../../lib/get_id'; -// @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; - -import { StorybookParams } from '.'; -import { CanvasWorkpadService } from '../workpad'; - -import * as stubs from '../stubs/workpad'; - -export { - findNoTemplates, - findNoWorkpads, - findSomeTemplates, - getNoTemplates, - getSomeTemplates, -} from '../stubs/workpad'; - -type CanvasWorkpadServiceFactory = PluginServiceFactory; - -const TIMEOUT = 500; -const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time)); - -const { findNoTemplates, findNoWorkpads, findSomeTemplates, importWorkpad } = stubs; - -const getRandomName = () => { - const lorem = - 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split( - ' ' - ); - return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' '); -}; - -const getRandomDate = ( - start: Date = moment().toDate(), - end: Date = moment().subtract(7, 'days').toDate() -) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString(); - -export const getSomeWorkpads = (count = 3) => - Array.from({ length: count }, () => ({ - '@created': getRandomDate( - moment().subtract(3, 'days').toDate(), - moment().subtract(10, 'days').toDate() - ), - '@timestamp': getRandomDate(), - id: getId('workpad'), - name: getRandomName(), - })); - -export const findSomeWorkpads = - (count = 3, useStaticData = false, timeout = TIMEOUT) => - (_term: string) => { - return Promise.resolve() - .then(promiseTimeout(timeout)) - .then(() => ({ - total: count, - workpads: useStaticData ? stubs.getSomeWorkpads(count) : getSomeWorkpads(count), - })); - }; - -export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ - workpadCount, - hasTemplates, - useStaticData, -}) => ({ - get: (id: string) => { - action('workpadService.get')(id); - return Promise.resolve({ ...getDefaultWorkpad(), id }); - }, - resolve: (id: string) => { - action('workpadService.resolve')(id); - return Promise.resolve({ outcome: 'exactMatch', workpad: { ...getDefaultWorkpad(), id } }); - }, - findTemplates: () => { - action('workpadService.findTemplates')(); - return (hasTemplates ? findSomeTemplates() : findNoTemplates())(); - }, - import: (workpad) => { - action('workpadService.import')(workpad); - return importWorkpad(workpad); - }, - create: (workpad) => { - action('workpadService.create')(workpad); - return Promise.resolve(workpad); - }, - createFromTemplate: (templateId: string) => { - action('workpadService.createFromTemplate')(templateId); - return Promise.resolve(getDefaultWorkpad()); - }, - find: (term: string) => { - action('workpadService.find')(term); - return (workpadCount ? findSomeWorkpads(workpadCount, useStaticData) : findNoWorkpads())(term); - }, - remove: (id: string) => { - action('workpadService.remove')(id); - return Promise.resolve(); - }, - update: (id, workpad) => { - action('worpadService.update')(workpad, id); - return Promise.resolve(); - }, - updateWorkpad: (id, workpad) => { - action('workpadService.updateWorkpad')(workpad, id); - return Promise.resolve(); - }, - updateAssets: (id, assets) => { - action('workpadService.updateAssets')(assets, id); - return Promise.resolve(); - }, - getRuntimeZip: (workpad) => - Promise.resolve(new Blob([JSON.stringify(workpad)], { type: 'application/json' })), -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/custom_element.ts b/x-pack/plugins/canvas/public/services/stubs/custom_element.ts deleted file mode 100644 index 551c06ef4b65ee..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/custom_element.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasCustomElementService } from '../custom_element'; - -type CanvasCustomElementServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -export const customElementServiceFactory: CanvasCustomElementServiceFactory = () => ({ - create: noop, - find: noop, - get: noop, - remove: noop, - update: noop, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/data_views.ts b/x-pack/plugins/canvas/public/services/stubs/data_views.ts deleted file mode 100644 index 1b1227dba4d3fd..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/data_views.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasDataViewsService } from '../data_views'; - -type DataViewsServiceFactory = PluginServiceFactory; - -export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({ - getDataViews: () => - Promise.resolve([ - { id: 'dataview1', title: 'dataview1', name: 'Data view 1' }, - { id: 'dataview2', title: 'dataview2', name: 'Data view 2' }, - ]), - getFields: () => Promise.resolve(['field1', 'field2']), - getDefaultDataView: () => - Promise.resolve({ - id: 'defaultDataViewId', - title: 'defaultDataView', - name: 'Default data view', - }), -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts deleted file mode 100644 index 95131efec2bde9..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasEmbeddablesService } from '../embeddables'; - -type EmbeddablesServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -export const embeddablesServiceFactory: EmbeddablesServiceFactory = () => ({ - reactEmbeddableRegistryHasKey: noop, - getReactEmbeddableSavedObjects: noop, - getEmbeddableFactories: noop, - getStateTransfer: noop, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/stubs/expressions.ts deleted file mode 100644 index 177eb1e50d75fb..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/expressions.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AnyExpressionRenderDefinition } from '@kbn/expressions-plugin/common'; -import { plugin } from '@kbn/expressions-plugin/public'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { functions as functionDefinitions } from '../../../canvas_plugin_src/functions/common'; -import { renderFunctions } from '../../../canvas_plugin_src/renderers/core'; -import { - CanvasExpressionsService, - CanvasExpressionsServiceRequiredServices, - ExpressionsService, -} from '../kibana/expressions'; - -type CanvasExpressionsServiceFactory = PluginServiceFactory< - CanvasExpressionsService, - {}, - CanvasExpressionsServiceRequiredServices ->; - -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( - params, - requiredServices -) => { - const placeholder = {} as any; - const expressionsPlugin = plugin(placeholder); - const setup = expressionsPlugin.setup(placeholder); - const fork = setup.fork('canvas'); - const expressionsService = fork.setup(); - - functionDefinitions.forEach((fn) => expressionsService.registerFunction(fn)); - renderFunctions.forEach((fn) => { - setup.registerRenderer(fn as unknown as AnyExpressionRenderDefinition); - }); - - return new ExpressionsService(fork.start(), requiredServices); -}; diff --git a/x-pack/plugins/canvas/public/services/stubs/filters.ts b/x-pack/plugins/canvas/public/services/stubs/filters.ts deleted file mode 100644 index 19d237bb9c390a..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/filters.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 { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasFiltersService } from '../filters'; - -export type CanvasFiltersServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -export const filtersServiceFactory: CanvasFiltersServiceFactory = () => ({ - getFilters: () => [ - 'exactly value="machine-learning" column="project1" filterGroup="Group 1"', - 'exactly value="kibana" column="project2" filterGroup="Group 1"', - 'time column="@timestamp1" from="2021-11-02 17:13:18" to="2021-11-09 17:13:18" filterGroup="Some group"', - ], - updateFilter: noop, - getFiltersContext: () => ({ variables: {} }), -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts deleted file mode 100644 index c129441b078258..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ /dev/null @@ -1,61 +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. - */ - -export * from '../legacy/stubs'; - -import { - PluginServiceProviders, - PluginServiceProvider, - PluginServiceRegistry, -} from '@kbn/presentation-util-plugin/public'; - -import { CanvasPluginServices } from '..'; -import { customElementServiceFactory } from './custom_element'; -import { dataViewsServiceFactory } from './data_views'; -import { embeddablesServiceFactory } from './embeddables'; -import { expressionsServiceFactory } from './expressions'; -import { labsServiceFactory } from './labs'; -import { navLinkServiceFactory } from './nav_link'; -import { notifyServiceFactory } from './notify'; -import { platformServiceFactory } from './platform'; -import { reportingServiceFactory } from './reporting'; -import { visualizationsServiceFactory } from './visualizations'; -import { workpadServiceFactory } from './workpad'; -import { filtersServiceFactory } from './filters'; -import { uiActionsServiceFactory } from './ui_actions'; - -export { customElementServiceFactory } from './custom_element'; -export { dataViewsServiceFactory } from './data_views'; -export { expressionsServiceFactory } from './expressions'; -export { filtersServiceFactory } from './filters'; -export { labsServiceFactory } from './labs'; -export { navLinkServiceFactory } from './nav_link'; -export { notifyServiceFactory } from './notify'; -export { platformServiceFactory } from './platform'; -export { reportingServiceFactory } from './reporting'; -export { visualizationsServiceFactory } from './visualizations'; -export { workpadServiceFactory } from './workpad'; - -export const pluginServiceProviders: PluginServiceProviders = { - customElement: new PluginServiceProvider(customElementServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), - filters: new PluginServiceProvider(filtersServiceFactory), - labs: new PluginServiceProvider(labsServiceFactory), - navLink: new PluginServiceProvider(navLinkServiceFactory), - notify: new PluginServiceProvider(notifyServiceFactory), - platform: new PluginServiceProvider(platformServiceFactory), - reporting: new PluginServiceProvider(reportingServiceFactory), - visualizations: new PluginServiceProvider(visualizationsServiceFactory), - workpad: new PluginServiceProvider(workpadServiceFactory), - uiActions: new PluginServiceProvider(uiActionsServiceFactory), -}; - -export const pluginServiceRegistry = new PluginServiceRegistry( - pluginServiceProviders -); diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/stubs/labs.ts deleted file mode 100644 index d563126a15da33..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/labs.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 { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { projectIDs } from '@kbn/presentation-util-plugin/common'; -import { CanvasLabsService } from '../labs'; - -type CanvasLabsServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -export const labsServiceFactory: CanvasLabsServiceFactory = () => ({ - getProject: noop, - getProjects: noop, - getProjectIDs: () => projectIDs, - isProjectEnabled: () => false, - isLabsEnabled: () => true, - projectIDs, - reset: noop, - setProjectStatus: noop, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/nav_link.ts b/x-pack/plugins/canvas/public/services/stubs/nav_link.ts deleted file mode 100644 index becbdebcd63287..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/nav_link.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 { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasNavLinkService } from '../nav_link'; - -type CanvasNavLinkServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -export const navLinkServiceFactory: CanvasNavLinkServiceFactory = () => ({ - updatePath: noop, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/notify.ts b/x-pack/plugins/canvas/public/services/stubs/notify.ts deleted file mode 100644 index 2406afe3ca8a2b..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/notify.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { CanvasNotifyService } from '../notify'; - -type CanvasNotifyServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -export const notifyServiceFactory: CanvasNotifyServiceFactory = () => ({ - error: noop, - info: noop, - success: noop, - warning: noop, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts deleted file mode 100644 index 07268100752519..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/platform.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { CanvasPlatformService } from '../platform'; - -type CanvasPlatformServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -const uiSettings: Record = { - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', -}; - -const getUISetting = (setting: string) => uiSettings[setting]; - -export const platformServiceFactory: CanvasPlatformServiceFactory = () => ({ - getBasePath: () => '/base/path', - getBasePathInterface: noop, - getDocLinkVersion: () => 'docLinkVersion', - getElasticWebsiteUrl: () => 'https://elastic.co', - getKibanaVersion: () => 'kibanaVersion', - getHasWriteAccess: () => true, - getUISetting, - hasHeaderBanner$: noop, - setBreadcrumbs: noop, - setRecentlyAccessed: noop, - getUISettings: noop, - setFullscreen: noop, - redirectLegacyUrl: noop, - getLegacyUrlConflict: undefined, - getHttp: noop, - getContentManagement: noop, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/reporting.tsx b/x-pack/plugins/canvas/public/services/stubs/reporting.tsx deleted file mode 100644 index 71c376efbaba96..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/reporting.tsx +++ /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 React from 'react'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -import { CanvasReportingService } from '../reporting'; - -type CanvasReportingServiceFactory = PluginServiceFactory; - -export const reportingServiceFactory: CanvasReportingServiceFactory = () => ({ - getReportingPanelPDFComponent: () => () =>
Reporting Panel PDF
, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/ui_actions.ts b/x-pack/plugins/canvas/public/services/stubs/ui_actions.ts deleted file mode 100644 index 84039e0888be8e..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/ui_actions.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 { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; -import { CanvasUiActionsService } from '../ui_actions'; - -type UiActionsServiceFactory = PluginServiceFactory; - -export const uiActionsServiceFactory: UiActionsServiceFactory = () => { - const pluginMock = uiActionsPluginMock.createStartContract(); - return { getTriggerCompatibleActions: pluginMock.getTriggerCompatibleActions }; -}; diff --git a/x-pack/plugins/canvas/public/services/stubs/visualizations.ts b/x-pack/plugins/canvas/public/services/stubs/visualizations.ts deleted file mode 100644 index 812287bf078fb0..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/visualizations.ts +++ /dev/null @@ -1,19 +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 { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { CanvasVisualizationsService } from '../visualizations'; - -type VisualizationsServiceFactory = PluginServiceFactory; - -const noop = (..._args: any[]): any => {}; - -export const visualizationsServiceFactory: VisualizationsServiceFactory = () => ({ - showNewVisModal: noop, - getByGroup: noop, - getAliases: noop, -}); diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts deleted file mode 100644 index 51be4e150ebe56..00000000000000 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ /dev/null @@ -1,115 +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 moment from 'moment'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; - -// @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; -import { CanvasWorkpadService } from '../workpad'; -import { CanvasTemplate, CanvasWorkpad } from '../../../types'; - -type CanvasWorkpadServiceFactory = PluginServiceFactory; - -export const TIMEOUT = 500; -export const promiseTimeout = (time: number) => () => - new Promise((resolve) => setTimeout(resolve, time)); - -const DAY = 86400000; -const JAN_1_2000 = 946684800000; - -const getWorkpads = (count = 3) => { - const workpads = []; - for (let i = 0; i < count; i++) { - workpads[i] = { - ...getDefaultWorkpad(), - name: `Workpad ${i}`, - id: `workpad-${i}`, - '@created': moment(JAN_1_2000 + DAY * i).toDate(), - '@timestamp': moment(JAN_1_2000 + DAY * (i + 1)).toDate(), - }; - } - return workpads; -}; - -export const getSomeWorkpads = (count = 3) => getWorkpads(count); - -export const findSomeWorkpads = - (count = 3, timeout = TIMEOUT) => - (_term: string) => { - return Promise.resolve() - .then(promiseTimeout(timeout)) - .then(() => ({ - total: count, - workpads: getSomeWorkpads(count), - })); - }; - -const templates: CanvasTemplate[] = [ - { - id: 'test1-id', - name: 'test1', - help: 'This is a test template', - tags: ['tag1', 'tag2'], - template_key: 'test1-key', - }, - { - id: 'test2-id', - name: 'test2', - help: 'This is a second test template', - tags: ['tag2', 'tag3'], - template_key: 'test2-key', - }, -]; - -export const findNoWorkpads = - (timeout = TIMEOUT) => - (_term: string) => { - return Promise.resolve() - .then(promiseTimeout(timeout)) - .then(() => ({ - total: 0, - workpads: [], - })); - }; - -export const findSomeTemplates = - (timeout = TIMEOUT) => - () => { - return Promise.resolve() - .then(promiseTimeout(timeout)) - .then(() => getSomeTemplates()); - }; - -export const findNoTemplates = - (timeout = TIMEOUT) => - () => { - return Promise.resolve() - .then(promiseTimeout(timeout)) - .then(() => getNoTemplates()); - }; - -export const importWorkpad = (workpad: CanvasWorkpad) => Promise.resolve(workpad); -export const getNoTemplates = () => ({ templates: [] }); -export const getSomeTemplates = () => ({ templates }); - -export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({ - get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }), - resolve: (id: string) => - Promise.resolve({ outcome: 'exactMatch', workpad: { ...getDefaultWorkpad(), id } }), - findTemplates: findNoTemplates(), - create: (workpad) => Promise.resolve(workpad), - import: (workpad) => importWorkpad(workpad), - createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()), - find: findNoWorkpads(), - remove: (_id: string) => Promise.resolve(), - update: (id, workpad) => Promise.resolve(), - updateWorkpad: (id, workpad) => Promise.resolve(), - updateAssets: (id, assets) => Promise.resolve(), - getRuntimeZip: (workpad) => - Promise.resolve(new Blob([JSON.stringify(workpad)], { type: 'application/json' })), -}); diff --git a/x-pack/plugins/canvas/public/services/ui_actions.ts b/x-pack/plugins/canvas/public/services/ui_actions.ts deleted file mode 100644 index 62f99facce023d..00000000000000 --- a/x-pack/plugins/canvas/public/services/ui_actions.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; - -export interface CanvasUiActionsService { - getTriggerCompatibleActions: UiActionsStart['getTriggerCompatibleActions']; -} diff --git a/x-pack/plugins/canvas/public/services/visualizations.ts b/x-pack/plugins/canvas/public/services/visualizations.ts deleted file mode 100644 index dc816f764e5ec4..00000000000000 --- a/x-pack/plugins/canvas/public/services/visualizations.ts +++ /dev/null @@ -1,14 +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 { VisualizationsStart } from '@kbn/visualizations-plugin/public'; - -export interface CanvasVisualizationsService { - showNewVisModal: VisualizationsStart['showNewVisModal']; - getByGroup: VisualizationsStart['getByGroup']; - getAliases: VisualizationsStart['getAliases']; -} diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts deleted file mode 100644 index 72996f54c158d4..00000000000000 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ResolvedSimpleSavedObject } from '@kbn/core/public'; -import { CanvasWorkpad, CanvasTemplate } from '../../types'; -import type { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; - -export type FoundWorkpads = Array>; -export type FoundWorkpad = FoundWorkpads[number]; -export interface WorkpadFindResponse { - total: number; - workpads: FoundWorkpads; -} - -export interface TemplateFindResponse { - templates: CanvasTemplate[]; -} - -export interface ResolveWorkpadResponse { - workpad: CanvasWorkpad; - outcome: ResolvedSimpleSavedObject['outcome']; - aliasId?: ResolvedSimpleSavedObject['alias_target_id']; - aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose']; -} - -export interface CanvasWorkpadService { - get: (id: string) => Promise; - resolve: (id: string) => Promise; - create: (workpad: CanvasWorkpad) => Promise; - import: (workpad: CanvasWorkpad) => Promise; - createFromTemplate: (templateId: string) => Promise; - find: (term: string) => Promise; - remove: (id: string) => Promise; - findTemplates: () => Promise; - update: (id: string, workpad: CanvasWorkpad) => Promise; - updateWorkpad: (id: string, workpad: CanvasWorkpad) => Promise; - updateAssets: (id: string, assets: CanvasWorkpad['assets']) => Promise; - getRuntimeZip: (workpad: CanvasRenderedWorkpad) => Promise; -} diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index b94a198ee4be95..57b7da1ce1fef3 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -23,9 +23,11 @@ import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; import { ErrorStrings } from '../../../i18n'; import { subMultitree } from '../../lib/aeroelastic/functional'; -import { pluginServices } from '../../services'; +import { getCanvasExpressionService } from '../../services/canvas_expressions_service'; +import { getCanvasNotifyService } from '../../services/canvas_notify_service'; import { selectToplevelNodes } from './transient'; import * as args from './resolved_args'; +import { setFilter } from './filters'; const { actionsElements: strings } = ErrorStrings; @@ -101,7 +103,7 @@ const fetchContextFn = ({ dispatch, getState }, index, element, fullRefresh = fa const variables = getWorkpadVariablesAsObject(getState()); - const { expressions } = pluginServices.getServices(); + const expressions = getCanvasExpressionService(); const elementWithNewAst = set(element, pathToTarget, astChain); // get context data from a partial AST @@ -129,7 +131,8 @@ const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, cont }); const variables = getWorkpadVariablesAsObject(getState()); - const { expressions, notify } = pluginServices.getServices(); + const expressions = getCanvasExpressionService(); + const notify = getCanvasNotifyService(); return expressions .runInterpreter(ast, context, variables, { castToRender: true }) @@ -177,7 +180,8 @@ export const fetchAllRenderables = createThunk( const argumentPath = [element.id, 'expressionRenderable']; const variables = getWorkpadVariablesAsObject(getState()); - const { expressions, notify } = pluginServices.getServices(); + const expressions = getCanvasExpressionService(); + const notify = getCanvasNotifyService(); return expressions .runInterpreter(ast, null, variables, { castToRender: true }) @@ -260,18 +264,6 @@ export const removeElements = createThunk( } ); -export const setFilter = createThunk( - 'setFilter', - ({ dispatch }, filter, elementId, doRender = true) => { - const _setFilter = createAction('setFilter'); - dispatch(_setFilter({ filter, elementId })); - - if (doRender === true) { - dispatch(fetchAllRenderables()); - } - } -); - export const setExpression = createThunk('setExpression', setExpressionFn); function setExpressionFn({ dispatch, getState }, expression, elementId, pageId, doRender = true) { // dispatch action to update the element in state @@ -290,7 +282,11 @@ function setExpressionFn({ dispatch, getState }, expression, elementId, pageId, ) ) { const filter = ''; - dispatch(setFilter(filter, elementId, pageId, doRender)); + dispatch(setFilter(filter, elementId, pageId)); + + if (doRender) { + dispatch(fetchAllRenderables(updatedElement)); + } // setFilter will trigger a re-render so we can skip the fetch here } else if (doRender === true) { dispatch(fetchRenderable(updatedElement)); @@ -302,7 +298,7 @@ const setAst = createThunk('setAst', ({ dispatch }, ast, element, pageId, doRend const expression = toExpression(ast); dispatch(setExpression(expression, element.id, pageId, doRender)); } catch (err) { - const notifyService = pluginServices.getServices().notify; + const notifyService = getCanvasNotifyService(); notifyService.error(err); // TODO: remove this, may have been added just to cause a re-render, but why? diff --git a/x-pack/plugins/canvas/public/state/actions/filters.js b/x-pack/plugins/canvas/public/state/actions/filters.js new file mode 100644 index 00000000000000..3347925dc5a15e --- /dev/null +++ b/x-pack/plugins/canvas/public/state/actions/filters.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createAction } from 'redux-actions'; +import { createThunk } from '../../lib/create_thunk'; + +export const setFilter = createThunk('setFilter', ({ dispatch }, filter, elementId) => { + const _setFilter = createAction('setFilter'); + dispatch(_setFilter({ filter, elementId })); +}); diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index 88330c92d867b7..fd509c952ef187 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -6,18 +6,15 @@ */ import { get } from 'lodash'; -import { pluginServices } from '../services'; +import { coreServices } from '../services/kibana_services'; import { getDefaultWorkpad, getDefaultSidebar, getDefaultFlyouts } from './defaults'; export const getInitialState = (path) => { - const platformService = pluginServices.getServices().platform; - const { getHasWriteAccess } = platformService; - const state = { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - canUserWrite: getHasWriteAccess(), + canUserWrite: coreServices.application.capabilities.canvas.save, zoomScale: 1, elementStats: { total: 0, diff --git a/x-pack/plugins/canvas/public/state/reducers/elements.js b/x-pack/plugins/canvas/public/state/reducers/elements.js index 2be1cf519ee133..fdb15a56212224 100644 --- a/x-pack/plugins/canvas/public/state/reducers/elements.js +++ b/x-pack/plugins/canvas/public/state/reducers/elements.js @@ -8,7 +8,6 @@ import { handleActions } from 'redux-actions'; import immutable from 'object-path-immutable'; import { get } from 'lodash'; -import * as actions from '../actions/elements'; const { assign, push, del, set } = immutable; @@ -102,17 +101,16 @@ const getPageWithElementId = (workpad, elementId) => { export const elementsReducer = handleActions( { - // TODO: This takes the entire element, which is not necessary, it could just take the id. - [actions.setExpression]: (workpadState, { payload }) => { + ['setExpression']: (workpadState, { payload }) => { const { expression, pageId, elementId } = payload; return assignNodeProperties(workpadState, pageId, elementId, { expression }); }, - [actions.setFilter]: (workpadState, { payload }) => { + ['setFilter']: (workpadState, { payload }) => { const { filter, elementId } = payload; const pageId = getPageWithElementId(workpadState, elementId); return assignNodeProperties(workpadState, pageId, elementId, { filter }); }, - [actions.setMultiplePositions]: (workpadState, { payload }) => + ['setMultiplePositions']: (workpadState, { payload }) => payload.repositionedElements.reduce( (previousWorkpadState, { position, pageId, elementId }) => assignNodeProperties(previousWorkpadState, pageId, elementId, { @@ -120,11 +118,11 @@ export const elementsReducer = handleActions( }), workpadState ), - [actions.elementLayer]: (workpadState, { payload: { pageId, elementId, movement } }) => { + ['elementLayer']: (workpadState, { payload: { pageId, elementId, movement } }) => { const location = getLocationFromIds(workpadState, pageId, elementId); return moveNodeLayer(workpadState, pageId, elementId, movement, location); }, - [actions.addElement]: (workpadState, { payload: { pageId, element } }) => { + ['addElement']: (workpadState, { payload: { pageId, element } }) => { const pageIndex = getPageIndexById(workpadState, pageId); if (pageIndex < 0) { return workpadState; @@ -143,7 +141,7 @@ export const elementsReducer = handleActions( trimElement(element) ); }, - [actions.insertNodes]: (workpadState, { payload: { pageId, elements } }) => { + ['insertNodes']: (workpadState, { payload: { pageId, elements } }) => { const pageIndex = getPageIndexById(workpadState, pageId); if (pageIndex < 0) { return workpadState; @@ -158,7 +156,7 @@ export const elementsReducer = handleActions( workpadState ); }, - [actions.removeElements]: (workpadState, { payload: { pageId, elementIds } }) => { + ['removeElements']: (workpadState, { payload: { pageId, elementIds } }) => { const pageIndex = getPageIndexById(workpadState, pageId); if (pageIndex < 0) { return workpadState; diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts index 5b963e2c80c7e3..8f79ed529cd968 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts @@ -9,10 +9,7 @@ import { fromExpression, toExpression } from '@kbn/interpreter'; import { handleActions } from 'redux-actions'; import { State } from '../../../types'; -import { - UpdateEmbeddableExpressionActionType, - UpdateEmbeddableExpressionPayload, -} from '../actions/embeddable'; +import { UpdateEmbeddableExpressionPayload } from '../actions/embeddable'; // @ts-expect-error untyped local import { assignNodeProperties } from './elements'; @@ -22,7 +19,7 @@ export const embeddableReducer = handleActions< UpdateEmbeddableExpressionPayload >( { - [UpdateEmbeddableExpressionActionType]: (workpadState, { payload }) => { + ['updateEmbeddableExpression']: (workpadState, { payload }) => { if (!payload) { return workpadState; } diff --git a/x-pack/plugins/canvas/public/state/reducers/resolved_args.js b/x-pack/plugins/canvas/public/state/reducers/resolved_args.js index 3514036cafc0df..3652a8d462fb82 100644 --- a/x-pack/plugins/canvas/public/state/reducers/resolved_args.js +++ b/x-pack/plugins/canvas/public/state/reducers/resolved_args.js @@ -10,8 +10,6 @@ import immutable from 'object-path-immutable'; import { get } from 'lodash'; import { prepend } from '../../lib/modify_path'; import * as actions from '../actions/resolved_args'; -import { flushContext, flushContextAfterIndex } from '../actions/elements'; -import { setWorkpad } from '../actions/workpad'; const { set, del } = immutable; /* @@ -114,14 +112,14 @@ export const resolvedArgsReducer = handleActions( /* * Flush all cached contexts */ - [flushContext]: (transientState, { payload: elementId }) => { + ['flushContext']: (transientState, { payload: elementId }) => { return del(transientState, getFullPath([elementId, 'expressionContext'])); }, /* * Flush cached context indices from the given index to the last */ - [flushContextAfterIndex]: (transientState, { payload }) => { + ['flushContextAfterIndex']: (transientState, { payload }) => { const { elementId, index } = payload; const expressionContext = get(transientState, getFullPath([elementId, 'expressionContext'])); @@ -139,7 +137,7 @@ export const resolvedArgsReducer = handleActions( return state; }, transientState); }, - [setWorkpad]: (transientState, {}) => { + ['setWorkpad']: (transientState, {}) => { return set(transientState, 'resolvedArgs', {}); }, }, diff --git a/x-pack/plugins/canvas/public/state/reducers/transient.js b/x-pack/plugins/canvas/public/state/reducers/transient.js index e41ab5e4822a97..2f0e7bd66126c0 100644 --- a/x-pack/plugins/canvas/public/state/reducers/transient.js +++ b/x-pack/plugins/canvas/public/state/reducers/transient.js @@ -7,11 +7,6 @@ import { handleActions } from 'redux-actions'; import immutable from 'object-path-immutable'; -import { restoreHistory } from '../actions/history'; -import * as pageActions from '../actions/pages'; -import * as transientActions from '../actions/transient'; -import { removeElements } from '../actions/elements'; -import { setRefreshInterval, enableAutoplay, setAutoplayInterval } from '../actions/workpad'; const { set, del } = immutable; @@ -19,9 +14,9 @@ export const transientReducer = handleActions( { // clear all the resolved args when restoring the history // TODO: we shouldn't need to reset the resolved args for history - [restoreHistory]: (transientState) => set(transientState, 'resolvedArgs', {}), + ['restoreHistory']: (transientState) => set(transientState, 'resolvedArgs', {}), - [removeElements]: (transientState, { payload: { elementIds } }) => { + ['removeElements']: (transientState, { payload: { elementIds } }) => { const { selectedToplevelNodes } = transientState; return del( { @@ -32,53 +27,53 @@ export const transientReducer = handleActions( ); }, - [transientActions.setFirstLoad]: (transientState, { payload }) => { + ['setFirstLoad']: (transientState, { payload }) => { return set(transientState, 'isFirstLoad', Boolean(payload)); }, - [transientActions.setFullscreen]: (transientState, { payload }) => { + ['setFullscreen']: (transientState, { payload }) => { return set(transientState, 'fullscreen', Boolean(payload)); }, - [transientActions.setElementStats]: (transientState, { payload }) => { + ['setElementStats']: (transientState, { payload }) => { return set(transientState, 'elementStats', payload); }, - [transientActions.selectToplevelNodes]: (transientState, { payload }) => { + ['selectToplevelNodes']: (transientState, { payload }) => { return { ...transientState, selectedToplevelNodes: payload, }; }, - [transientActions.setZoomScale]: (transientState, { payload }) => { + ['setZoomScale']: (transientState, { payload }) => { return { ...transientState, zoomScale: payload || 1, }; }, - [pageActions.setPage]: (transientState) => { + ['setPage']: (transientState) => { return { ...transientState, selectedToplevelNodes: [] }; }, - [pageActions.addPage]: (transientState) => { + ['addPage']: (transientState) => { return { ...transientState, selectedToplevelNodes: [] }; }, - [pageActions.duplicatePage]: (transientState) => { + ['duplicatePage']: (transientState) => { return { ...transientState, selectedToplevelNodes: [] }; }, - [setRefreshInterval]: (transientState, { payload }) => { + ['setRefreshInterval']: (transientState, { payload }) => { return { ...transientState, refresh: { interval: Number(payload) || 0 } }; }, - [enableAutoplay]: (transientState, { payload }) => { + ['enableAutoplay']: (transientState, { payload }) => { return set(transientState, 'autoplay.enabled', Boolean(payload) || false); }, - [setAutoplayInterval]: (transientState, { payload }) => { + ['setAutoplayInterval']: (transientState, { payload }) => { return set(transientState, 'autoplay.interval', Number(payload) || 0); }, }, diff --git a/x-pack/plugins/canvas/public/state/reducers/workpad.js b/x-pack/plugins/canvas/public/state/reducers/workpad.js index ebde0106f9c012..9c337f965ae3aa 100644 --- a/x-pack/plugins/canvas/public/state/reducers/workpad.js +++ b/x-pack/plugins/canvas/public/state/reducers/workpad.js @@ -6,66 +6,52 @@ */ import { handleActions } from 'redux-actions'; -import { pluginServices } from '../../services'; +import { coreServices } from '../../services/kibana_services'; import { getDefaultWorkpad } from '../defaults'; -import { - setWorkpad, - sizeWorkpad, - setColors, - setName, - setWriteable, - setWorkpadCSS, - setWorkpadVariables, - resetWorkpad, -} from '../actions/workpad'; import { APP_ROUTE_WORKPAD } from '../../../common/lib/constants'; export const workpadReducer = handleActions( { - [setWorkpad]: (workpadState, { payload }) => { - pluginServices - .getServices() - .platform.setRecentlyAccessed( - `${APP_ROUTE_WORKPAD}/${payload.id}`, - payload.name, - payload.id - ); + ['setWorkpad']: (workpadState, { payload }) => { + coreServices.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${payload.id}`, + payload.name, + payload.id + ); return payload; }, - [sizeWorkpad]: (workpadState, { payload }) => { + ['sizeWorkpad']: (workpadState, { payload }) => { return { ...workpadState, ...payload }; }, - [setColors]: (workpadState, { payload }) => { + ['setColors']: (workpadState, { payload }) => { return { ...workpadState, colors: payload }; }, - [setName]: (workpadState, { payload }) => { - pluginServices - .getServices() - .platform.setRecentlyAccessed( - `${APP_ROUTE_WORKPAD}/${workpadState.id}`, - payload, - workpadState.id - ); + ['setName']: (workpadState, { payload }) => { + coreServices.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${workpadState.id}`, + payload, + workpadState.id + ); return { ...workpadState, name: payload }; }, - [setWriteable]: (workpadState, { payload }) => { + ['setWriteable']: (workpadState, { payload }) => { return { ...workpadState, isWriteable: Boolean(payload) }; }, - [setWorkpadCSS]: (workpadState, { payload }) => { + ['setWorkpadCSS']: (workpadState, { payload }) => { return { ...workpadState, css: payload }; }, - [setWorkpadVariables]: (workpadState, { payload }) => { + ['setWorkpadVariables']: (workpadState, { payload }) => { return { ...workpadState, variables: payload }; }, - [resetWorkpad]: () => ({ ...getDefaultWorkpad() }), + ['resetWorkpad']: () => ({ ...getDefaultWorkpad() }), }, {} ); diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap index 010847e63a023a..4aa379aa194bc5 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap +++ b/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` App renders properly 1`] = `"
markdown mock
markdown mock

Page level controls

My Canvas Workpad

There is a new region landmark with page level controls at the end of the document.

"`; +exports[` App renders properly 1`] = `"
markdown mock
markdown mock

Page level controls

My Canvas Workpad

There is a new region landmark with page level controls at the end of the document.

"`; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index ec4ea09e8fcc7d..30d55c8531b1dd 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -13,6 +13,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; + // import { act } from 'react-dom/test-utils'; import { App } from './app'; import { sharedWorkpads, WorkpadNames, tick } from '../test'; @@ -34,17 +35,14 @@ import { openSettings, selectMenuItem } from '../test/interactions'; // Mock the renderers jest.mock('../supported_renderers'); +// @ts-ignore Importing this to mock +import * as Portal from '@elastic/eui/lib/components/portal/portal'; + // Mock the EuiPortal - `insertAdjacentElement is not supported in // `jsdom` 12. We're just going to render a `div` with the children // so the `enzyme` tests will be accurate. -jest.mock('@elastic/eui/lib/components/portal/portal', () => { - // Local constants are not supported in Jest mocks-- they must be - // imported within the mock. - // eslint-disable-next-line @typescript-eslint/no-shadow - const React = jest.requireActual('react'); - return { - EuiPortal: (props: any) =>
{props.children}
, - }; +jest.spyOn(Portal, 'EuiPortal').mockImplementation((props: any) => { + return
{props.children}
; }); const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap index bf66b5df800ecc..cfe41427b1ea1e 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` can navigate Autoplay Settings 1`] = ` -
+
@@ -94,7 +96,9 @@ exports[` can navigate Autoplay Settings 1`] = ` `; exports[` can navigate Autoplay Settings 2`] = ` -
+
@@ -338,7 +342,9 @@ exports[` can navigate Autoplay Settings 2`] = ` `; exports[` can navigate Toolbar Settings, closes when activated 1`] = ` -
+
@@ -431,7 +437,9 @@ exports[` can navigate Toolbar Settings, closes when activated 1`] = `; exports[` can navigate Toolbar Settings, closes when activated 2`] = ` -
+
@@ -606,7 +614,9 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] = `; exports[` can navigate Toolbar Settings, closes when activated 3`] = ` -
+
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.test.tsx index cc275dd983e3ce..0ab9e13e367651 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.test.tsx @@ -21,12 +21,14 @@ import { Settings } from './settings'; jest.mock('../../../supported_renderers'); -jest.mock('@elastic/eui/lib/components/portal/portal', () => { - // eslint-disable-next-line @typescript-eslint/no-shadow - const React = jest.requireActual('react'); - return { - EuiPortal: (props: any) =>
{props.children}
, - }; +// @ts-ignore Importing this to mock +import * as Portal from '@elastic/eui/lib/components/portal/portal'; + +// Mock the EuiPortal - `insertAdjacentElement is not supported in +// `jsdom` 12. We're just going to render a `div` with the children +// so the `enzyme` tests will be accurate. +jest.spyOn(Portal, 'EuiPortal').mockImplementation((props: any) => { + return
{props.children}
; }); describe('', () => { diff --git a/x-pack/plugins/canvas/shareable_runtime/test/selectors.ts b/x-pack/plugins/canvas/shareable_runtime/test/selectors.ts index bcfe94adf94be1..f4d4f8ac442d7e 100644 --- a/x-pack/plugins/canvas/shareable_runtime/test/selectors.ts +++ b/x-pack/plugins/canvas/shareable_runtime/test/selectors.ts @@ -42,7 +42,7 @@ export const getSettingsTrigger = (wrapper: ReactWrapper) => export const getPopover = (wrapper: ReactWrapper) => wrapper.find('EuiPopover'); -export const getPortal = (wrapper: ReactWrapper) => wrapper.find('EuiPortal'); +export const getPortal = (wrapper: ReactWrapper) => wrapper.find('.mockedEuiPortal'); export const getContextMenu = (wrapper: ReactWrapper) => wrapper.find('EuiContextMenuClass'); diff --git a/x-pack/plugins/canvas/storybook/constants.ts b/x-pack/plugins/canvas/storybook/constants.ts index 711b9391794522..4f0ad2a602da0f 100644 --- a/x-pack/plugins/canvas/storybook/constants.ts +++ b/x-pack/plugins/canvas/storybook/constants.ts @@ -8,3 +8,34 @@ import path from 'path'; export const KIBANA_ROOT = path.resolve(__dirname, '../../../..'); + +export const argTypes = { + hasTemplates: { + name: 'Has templates?', + type: { + name: 'boolean', + }, + defaultValue: true, + control: { + type: 'boolean', + }, + }, + useStaticData: { + name: 'Use static data?', + type: { + name: 'boolean', + }, + defaultValue: false, + control: { + type: 'boolean', + }, + }, + workpadCount: { + name: 'Number of workpads', + type: { name: 'number' }, + defaultValue: 5, + control: { + type: 'range', + }, + }, +}; diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx index b5bb0710c3de29..c99f5df184cd4e 100644 --- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -10,19 +10,10 @@ import React from 'react'; import { DecoratorFn } from '@storybook/react'; import { I18nProvider } from '@kbn/i18n-react'; -import { PluginServiceRegistry } from '@kbn/presentation-util-plugin/public'; -import { pluginServices, CanvasPluginServices } from '../../public/services'; -import { pluginServiceProviders, StorybookParams } from '../../public/services/storybook'; import { LegacyServicesProvider } from '../../public/services/legacy'; -import { startServices } from '../../public/services/legacy/stubs'; +import { setStubKibanaServices } from '../../public/services/mocks'; export const servicesContextDecorator = (): DecoratorFn => { - const pluginServiceRegistry = new PluginServiceRegistry( - pluginServiceProviders - ); - - pluginServices.setRegistry(pluginServiceRegistry.start({})); - return (story: Function, storybook) => { if (process.env.JEST_WORKER_ID !== undefined) { storybook.args.useStaticData = true; @@ -33,6 +24,7 @@ export const servicesContextDecorator = (): DecoratorFn => { }; export const legacyContextDecorator = () => { - startServices(); + setStubKibanaServices(); + return (story: Function) => {story()}; }; diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 85121b72c868c3..251deca87c4ee5 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -12,6 +12,7 @@ "allowJs": false }, "include": [ + "*.ts", "../../../typings/**/*", "__fixtures__/**/*", "canvas_plugin_src/**/*", @@ -88,9 +89,7 @@ "@kbn/presentation-containers", "@kbn/presentation-publishing", "@kbn/react-kibana-context-render", - "@kbn/search-types", + "@kbn/search-types" ], - "exclude": [ - "target/**/*", - ] + "exclude": ["target/**/*"] } From 445a2bd5baed49a7e07ab3e6c1b86bc1e5821496 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 21:39:29 +0100 Subject: [PATCH 49/50] skip flaky suite (#187182) --- x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts index d84d3aa691d1e6..ca0692b310606d 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts @@ -84,7 +84,8 @@ describe('Alert Event Details - Cases', { tags: ['@ess', '@serverless'] }, () => }); }); - describe('Case', () => { + // FLAKY: https://github.com/elastic/kibana/issues/187182 + describe.skip('Case', () => { let caseId: string; beforeEach(() => { From 2dfd8f1bedaa01453f8d1fbd4e6375cf1bca2843 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Oct 2024 21:47:25 +0100 Subject: [PATCH 50/50] skip flaky suite (#170371) --- .../response_actions/response_console/process_operations.cy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts index 9484122c013d7f..ed05f5a26e356d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts @@ -27,6 +27,7 @@ import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_ const AGENT_BEAT_FILE_PATH_SUFFIX = '/components/agentbeat'; // FLAKY: https://github.com/elastic/kibana/issues/170370 +// FLAKY: https://github.com/elastic/kibana/issues/170371 describe.skip('Response console', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { beforeEach(() => { login();