diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 36e1e3813275e..7052a19806d52 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -313,9 +313,11 @@ /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security +/x-pack/test/api_integration/apis/spaces/ @elastic/kibana-security /x-pack/test/ui_capabilities/ @elastic/kibana-security /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/test/functional/apps/security/ @elastic/kibana-security +/x-pack/test/functional/apps/spaces/ @elastic/kibana-security /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 8861681827ef3..b406ced798c0c 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -134,17 +134,19 @@ The `xpack.apm.autocreateApmIndexPattern` APM setting has been removed. For more *Impact* + To automatically create data views in APM, use `xpack.apm.autoCreateApmDataView`. ==== - + [discrete] [[deprecation-119494]] -.Updates Fleet API responses for consistency +.Updates Fleet API to improve consistency [%collapsible] ==== *Details* + -To make sure all Fleet API GET resposes return `items`, the following have been updated: +The Fleet API has been updated to improve consistency: -* `/api/fleet/enrollment-api-keys` -* `/api/fleet/agents` +* Hyphens are changed to underscores in some names. +* The `pkgkey` path parameter in the packages endpoint is split. +* The `response` and `list` properties are renamed to `items` or `item` in some +responses. For more information, refer to {kibana-pull}119494[#119494]. @@ -157,24 +159,30 @@ When you upgrade to 8.0.0, use the following API changes: * Use `service_tokens` instead of `service-tokens`. -* `check-permissions` is no longer supported. - * Use `/epm/packages/{packageName}/{version}` instead of `/epm/packages/{pkgkey}`. -* Use `items[]` or `item` instead of `response[]` in the following: - +* Use `items[]` instead of `response[]` in: ++ [source,text] -- +/api/fleet/enrollment_api_keys +/api/fleet/agents /epm/packages/ -/epm/packages/{pkgkey} /epm/categories /epm/packages/_bulk /epm/packages/limited +/epm/packages/{packageName}/{version} <1> -- +<1> Use `items[]` when the verb is `POST` or `DELETE`. Use `item` when the verb +is `GET` or `PUT`. + +For more information, refer to {fleet-guide}/fleet-api-docs.html[Fleet APIs]. + ==== -To review the depcrecations in previous versions, refer to the <>. - +To review the deprecations in previous versions, refer to the <>. + + [float] [[features-8.0.0-rc1]] === Features diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index d44de3c2efe2f..814a7d374506f 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -185,7 +185,7 @@ There are two things you can do to if you'd like to ensure a field is searchable 1. Index your additional data as {apm-guide-ref}/metadata.html[labels] instead. These are dynamic by default, which means they will be indexed and become searchable and aggregatable. -2. Use the {apm-guide-ref}/configuration-template.html[`append_fields`] feature. As an example, +2. Use the `append_fields` feature. As an example, adding the following to `apm-server.yml` will enable dynamic indexing for `http.request.cookies`: [source,yml] diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 5523201dce36f..c080c412f0f6b 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -16,7 +16,7 @@ NOTE: For emails to have a footer with a link back to {kib}, set the <"` format. See the https://nodemailer.com/message/addresses/[Nodemailer address documentation] for more information. +Sender:: The from address for all emails sent with this connector. This must be specified in `user@host-name` format. See the https://nodemailer.com/message/addresses/[Nodemailer address documentation] for more information. Service:: The name of the email service. If `service` is one of Nodemailer's https://nodemailer.com/smtp/well-known/[well-known email service providers], the `host`, `port`, and `secure` properties are defined with the default values and disabled for modification. If `service` is `MS Exchange Server`, the `host`, `port`, and `secure` properties are ignored and `tenantId`, `clientId`, `clientSecret` are required instead. If `service` is `other`, the `host` and `port` properties must be defined. Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts index 559c636cfaeec..4ea1af15f43ef 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts @@ -12,14 +12,14 @@ import { getApmWriteTargets } from '../../lib/apm/utils/get_apm_write_targets'; import { Scenario } from '../scenario'; import { getCommonServices } from '../utils/get_common_services'; -const scenario: Scenario = async ({ target, logLevel }) => { +const scenario: Scenario = async ({ target, logLevel, scenarioOpts }) => { const { client, logger } = getCommonServices({ target, logLevel }); const writeTargets = await getApmWriteTargets({ client }); + const { numServices = 3 } = scenarioOpts || {}; + return { generate: ({ from, to }) => { - const numServices = 3; - const range = timerange(from, to); const transactionName = '240rpm/75% 1000ms'; diff --git a/packages/elastic-apm-synthtrace/src/scripts/run.ts b/packages/elastic-apm-synthtrace/src/scripts/run.ts index 4078c848aa480..96bef3e958bdc 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/run.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/run.ts @@ -69,6 +69,12 @@ function options(y: Argv) { describe: 'Target to index', string: true, }) + .option('scenarioOpts', { + describe: 'Options specific to the scenario', + coerce: (arg) => { + return arg as Record | undefined; + }, + }) .conflicts('to', 'live'); } diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts index 5c081707bb75c..47359bd07aa8a 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/parse_run_cli_flags.ts @@ -47,7 +47,15 @@ export function parseRunCliFlags(flags: RunCliFlags) { } return { - ...pick(flags, 'target', 'workers', 'clientWorkers', 'batchSize', 'writeTarget'), + ...pick( + flags, + 'target', + 'workers', + 'clientWorkers', + 'batchSize', + 'writeTarget', + 'scenarioOpts' + ), intervalInMs, bucketSizeInMs, logLevel: parsedLogLevel, diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts index dd848d9f66c63..ee462085ef79c 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/start_historical_data_upload.ts @@ -24,6 +24,7 @@ export async function startHistoricalDataUpload({ target, file, writeTarget, + scenarioOpts, }: RunOptions & { from: number; to: number }) { let requestedUntil: number = from; @@ -57,6 +58,7 @@ export async function startHistoricalDataUpload({ target, workers, writeTarget, + scenarioOpts, }; const worker = new Worker(Path.join(__dirname, './upload_next_batch.js'), { diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts index 3610ffae3c7e6..ab4eee4f255b9 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/start_live_data_upload.ts @@ -24,6 +24,7 @@ export async function startLiveDataUpload({ logLevel, workers, writeTarget, + scenarioOpts, }: RunOptions & { start: number }) { let queuedEvents: ElasticsearchOutput[] = []; let requestedUntil: number = start; @@ -41,6 +42,7 @@ export async function startLiveDataUpload({ target, workers, writeTarget, + scenarioOpts, }); function uploadNextBatch() { diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts index c25fc7ca9f1c2..973cbc2266cbe 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/upload_next_batch.ts @@ -17,6 +17,7 @@ export interface WorkerData { bucketFrom: number; bucketTo: number; file: string; + scenarioOpts: Record | undefined; logLevel: LogLevel; clientWorkers: number; batchSize: number; @@ -39,6 +40,7 @@ const { workers, target, writeTarget, + scenarioOpts, } = workerData as WorkerData; async function uploadNextBatch() { @@ -63,6 +65,7 @@ async function uploadNextBatch() { target, workers, writeTarget, + scenarioOpts, }); const events = logger.perf('execute_scenario', () => diff --git a/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts b/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts index 69f10efd97d66..e3143a318a16e 100644 --- a/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts @@ -39,11 +39,9 @@ export const isQueryStringFilter = (filter: Filter): filter is QueryStringFilter * * @public */ -export const buildQueryFilter = (query: QueryStringFilter['query'], index: string, alias: string) => - ({ - query, - meta: { - index, - alias, - }, - } as QueryStringFilter); +export const buildQueryFilter = ( + query: QueryStringFilter['query'], + index: string, + alias?: string, + meta: QueryStringFilterMeta = {} +) => ({ query, meta: { index, alias, ...meta } }); diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 25e6c32705f4e..97b58ce5a700f 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -257,6 +257,18 @@ type MapRoutes = TRoutes extends [Route] MapRoute & MapRoute & MapRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : {}; // const element = null as any; diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts index f96f39349887e..b9fb8a21f0a8b 100644 --- a/src/core/server/elasticsearch/client/cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -144,13 +144,13 @@ describe('ClusterClient', () => { }); }); - it('creates a scoped facade with filtered auth headers', () => { + it('does not filter auth headers', () => { const config = createConfig({ requestHeadersWhitelist: ['authorization'], }); getAuthHeaders.mockReturnValue({ authorization: 'auth', - other: 'nope', + other: 'yep', }); const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); @@ -160,7 +160,12 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, + headers: { + ...DEFAULT_HEADERS, + authorization: 'auth', + other: 'yep', + 'x-opaque-id': expect.any(String), + }, }); }); @@ -170,7 +175,7 @@ describe('ClusterClient', () => { }); getAuthHeaders.mockReturnValue({ authorization: 'auth', - other: 'nope', + other: 'yep', }); const clusterClient = new ClusterClient(config, logger, 'custom-type', getAuthHeaders); @@ -184,7 +189,12 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, + headers: { + ...DEFAULT_HEADERS, + authorization: 'auth', + other: 'yep', + 'x-opaque-id': expect.any(String), + }, }); }); diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index 1f3118c77aa0f..1744d7a41841b 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -54,8 +54,6 @@ export interface ICustomClusterClient extends IClusterClient { export class ClusterClient implements ICustomClusterClient { public readonly asInternalUser: KibanaClient; private readonly rootScopedClient: KibanaClient; - private readonly allowListHeaders: string[]; - private isClosed = false; constructor( @@ -72,8 +70,6 @@ export class ClusterClient implements ICustomClusterClient { getExecutionContext, scoped: true, }); - - this.allowListHeaders = ['x-opaque-id', ...this.config.requestHeadersWhitelist]; } asScoped(request: ScopeableRequest) { @@ -95,14 +91,15 @@ export class ClusterClient implements ICustomClusterClient { private getScopedHeaders(request: ScopeableRequest): Headers { let scopedHeaders: Headers; if (isRealRequest(request)) { - const requestHeaders = ensureRawRequest(request).headers; + const requestHeaders = ensureRawRequest(request).headers ?? {}; const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {}; - const authHeaders = this.getAuthHeaders(request); + const authHeaders = this.getAuthHeaders(request) ?? {}; - scopedHeaders = filterHeaders( - { ...requestHeaders, ...requestIdHeaders, ...authHeaders }, - this.allowListHeaders - ); + scopedHeaders = { + ...filterHeaders(requestHeaders, this.config.requestHeadersWhitelist), + ...requestIdHeaders, + ...authHeaders, + }; } else { scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist); } diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 89a32d6dc5d2f..68a1993565209 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -61,7 +61,7 @@ export function filterHeaders( headers: Headers, fieldsToKeep: string[], fieldsToExclude: string[] = [] -) { +): Headers { const fieldsToExcludeNormalized = fieldsToExclude.map(normalizeHeaderField); // Normalize list of headers we want to allow in upstream request const fieldsToKeepNormalized = fieldsToKeep diff --git a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts index 3d8dcad08149c..b1c421ec9168a 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts @@ -67,7 +67,7 @@ describe('migration v2', () => { es: { license: 'basic', dataArchive: Path.join(__dirname, 'archives', '7.14.0_xpack_sample_saved_objects.zip'), - esArgs: ['http.max_content_length=1715275b'], + esArgs: ['http.max_content_length=1715329b'], }, }, })); @@ -85,7 +85,7 @@ describe('migration v2', () => { }); it('completes the migration even when a full batch would exceed ES http.max_content_length', async () => { - root = createRoot({ maxBatchSizeBytes: 1715275 }); + root = createRoot({ maxBatchSizeBytes: 1715329 }); esServer = await startES(); await root.preboot(); await root.setup(); @@ -109,7 +109,7 @@ describe('migration v2', () => { await root.preboot(); await root.setup(); await expect(root.start()).rejects.toMatchInlineSnapshot( - `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` + `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715329 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` ); await retryAsync( @@ -122,7 +122,7 @@ describe('migration v2', () => { expect( records.find((rec) => rec.message.startsWith( - `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` + `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715329 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` ) ) ).toBeDefined(); diff --git a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts index 33f00248a110a..0352e655937da 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts @@ -54,7 +54,7 @@ describe('migration v2', () => { }); it('fails with a descriptive message when maxBatchSizeBytes exceeds ES http.max_content_length', async () => { - root = createRoot({ maxBatchSizeBytes: 1715275 }); + root = createRoot({ maxBatchSizeBytes: 1715329 }); esServer = await startES(); await root.preboot(); await root.setup(); diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index aad45b6fc0bcf..c4be329dabcb8 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import React, { Fragment, useCallback, useState, ChangeEventHandler } from 'react'; +import React, { Fragment, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -23,22 +23,38 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiSwitch, - EuiSelect, - EuiFlexGroup, - EuiFlexItem, + EuiSuperSelect, } from '@elastic/eui'; import { DevToolsSettings } from '../../services'; export type AutocompleteOptions = 'fields' | 'indices' | 'templates'; -const PRESETS_IN_MINUTES = [1, 5, 10]; -const intervalOptions = PRESETS_IN_MINUTES.map((value) => ({ - value: value * 60000, - text: i18n.translate('console.settingsPage.refreshInterval.timeInterval', { - defaultMessage: '{value} {value, plural, one {minute} other {minutes}}', +const onceTimeInterval = () => + i18n.translate('console.settingsPage.refreshInterval.onceTimeInterval', { + defaultMessage: 'Once, when console loads', + }); + +const everyNMinutesTimeInterval = (value: number) => + i18n.translate('console.settingsPage.refreshInterval.everyNMinutesTimeInterval', { + defaultMessage: 'Every {value} {value, plural, one {minute} other {minutes}}', values: { value }, - }), + }); + +const everyHourTimeInterval = () => + i18n.translate('console.settingsPage.refreshInterval.everyHourTimeInterval', { + defaultMessage: 'Every hour', + }); + +const PRESETS_IN_MINUTES = [0, 1, 10, 20, 60]; +const intervalOptions = PRESETS_IN_MINUTES.map((value) => ({ + value: (value * 60000).toString(), + inputDisplay: + value === 0 + ? onceTimeInterval() + : value === 60 + ? everyHourTimeInterval() + : everyNMinutesTimeInterval(value), })); interface Props { @@ -112,10 +128,12 @@ export function DevToolsSettingsModal(props: Props) { }); } - const onIntervalChange: ChangeEventHandler = useCallback( - (e) => setPollInterval(parseInt(e.target.value, 10)), - [] - ); + const onPollingIntervalChange = useCallback((value: string) => { + const sanitizedValue = parseInt(value, 10); + + setPolling(!!sanitizedValue); + setPollInterval(sanitizedValue); + }, []); // It only makes sense to show polling options if the user needs to fetch any data. const pollingFields = @@ -125,43 +143,22 @@ export function DevToolsSettingsModal(props: Props) { label={ } helpText={ } > - - - - } - onChange={(e) => setPolling(e.target.checked)} - /> - - - - - + { + const { filters = [] } = input; + return { + type: 'filter', + filterType: 'filter', + and: filters.map(adaptToExpressionValueFilter), + }; + }, }, }; diff --git a/src/plugins/data/common/search/expressions/select_filter.test.ts b/src/plugins/data/common/search/expressions/select_filter.test.ts index a2515dbcb171d..8ef2b77b1fcc6 100644 --- a/src/plugins/data/common/search/expressions/select_filter.test.ts +++ b/src/plugins/data/common/search/expressions/select_filter.test.ts @@ -28,6 +28,12 @@ describe('interpreter/functions#selectFilter', () => { }, query: {}, }, + { + meta: { + group: 'g3', + }, + query: {}, + }, { meta: { group: 'g1', @@ -68,6 +74,12 @@ describe('interpreter/functions#selectFilter', () => { }, "query": Object {}, }, + Object { + "meta": Object { + "group": "g3", + }, + "query": Object {}, + }, Object { "meta": Object { "controlledBy": "i1", @@ -94,8 +106,8 @@ describe('interpreter/functions#selectFilter', () => { `); }); - it('selects filters belonging to certain group', () => { - const actual = fn(kibanaContext, { group: 'g1' }, createMockContext()); + it('selects filters belonging to certain groups', () => { + const actual = fn(kibanaContext, { group: ['g1', 'g3'] }, createMockContext()); expect(actual).toMatchInlineSnapshot(` Object { "filters": Array [ @@ -105,6 +117,12 @@ describe('interpreter/functions#selectFilter', () => { }, "query": Object {}, }, + Object { + "meta": Object { + "group": "g3", + }, + "query": Object {}, + }, Object { "meta": Object { "controlledBy": "i1", diff --git a/src/plugins/data/common/search/expressions/select_filter.ts b/src/plugins/data/common/search/expressions/select_filter.ts index 3e76f3a6426c2..600da4b16d274 100644 --- a/src/plugins/data/common/search/expressions/select_filter.ts +++ b/src/plugins/data/common/search/expressions/select_filter.ts @@ -11,7 +11,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { KibanaContext } from './kibana_context_type'; interface Arguments { - group?: string; + group: string[]; from?: string; ungrouped?: boolean; } @@ -37,6 +37,7 @@ export const selectFilterFunction: ExpressionFunctionSelectFilter = { help: i18n.translate('data.search.functions.selectFilter.group.help', { defaultMessage: 'Select only filters belonging to the provided group', }), + multi: true, }, from: { types: ['string'], @@ -54,13 +55,15 @@ export const selectFilterFunction: ExpressionFunctionSelectFilter = { }, }, - fn(input, { group, ungrouped, from }) { + fn(input, { group = [], ungrouped, from }) { return { ...input, filters: input.filters?.filter(({ meta }) => { const isGroupMatching = - (!group && !ungrouped) || group === meta.group || (ungrouped && !meta.group); + (!group.length && !ungrouped) || + (meta.group && group.length && group.includes(meta.group)) || + (ungrouped && !meta.group); const isOriginMatching = !from || from === meta.controlledBy; return isGroupMatching && isOriginMatching; }) || [], diff --git a/src/plugins/data/common/search/expressions/utils/filters_adapter.ts b/src/plugins/data/common/search/expressions/utils/filters_adapter.ts new file mode 100644 index 0000000000000..304150ad94813 --- /dev/null +++ b/src/plugins/data/common/search/expressions/utils/filters_adapter.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Filter } from '@kbn/es-query'; +import { ExpressionValueFilter } from 'src/plugins/expressions/common'; + +function getGroupFromFilter(filter: Filter) { + const { meta } = filter; + const { group } = meta ?? {}; + return group; +} + +function range(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { range: rangeQuery } = query ?? {}; + const column = Object.keys(rangeQuery)[0]; + const { gte: from, lte: to } = rangeQuery[column] ?? {}; + return { + filterGroup: getGroupFromFilter(filter), + from, + to, + column, + type: 'filter', + filterType: 'time', + and: [], + }; +} + +function luceneQueryString(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { query_string: queryString } = query ?? {}; + const { query: queryValue } = queryString; + + return { + filterGroup: getGroupFromFilter(filter), + query: queryValue, + type: 'filter', + filterType: 'luceneQueryString', + and: [], + }; +} + +function term(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { term: termQuery } = query ?? {}; + const column = Object.keys(termQuery)[0]; + const { value } = termQuery[column] ?? {}; + + return { + filterGroup: getGroupFromFilter(filter), + column, + value, + type: 'filter', + filterType: 'exactly', + and: [], + }; +} + +const adapters = { range, term, luceneQueryString }; + +export function adaptToExpressionValueFilter(filter: Filter): ExpressionValueFilter { + const { query = {} } = filter; + const filterType = Object.keys(query)[0] as keyof typeof adapters; + const adapt = adapters[filterType]; + if (!adapt || typeof adapt !== 'function') { + throw new Error(`Unknown filter type: ${filterType}`); + } + return adapt(filter); +} diff --git a/src/plugins/data/common/search/expressions/utils/index.ts b/src/plugins/data/common/search/expressions/utils/index.ts index a6ea8da6ac6e9..b678bd8781d93 100644 --- a/src/plugins/data/common/search/expressions/utils/index.ts +++ b/src/plugins/data/common/search/expressions/utils/index.ts @@ -7,3 +7,4 @@ */ export * from './function_wrapper'; +export { adaptToExpressionValueFilter } from './filters_adapter'; diff --git a/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap b/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap index 1d4a8614b2921..2714dbd2265a4 100644 --- a/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap +++ b/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap @@ -4,5 +4,6 @@ exports[`Executor .inject .getAllMigrations returns list of all registered migra Object { "7.10.0": [Function], "7.10.1": [Function], + "8.1.0": [Function], } `; diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 7e314788b03fd..be985c2720f8b 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -246,6 +246,40 @@ describe('Executor', () => { }); describe('.migrateToLatest', () => { + const fnMigrateTo = { + name: 'fnMigrateTo', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + fn: jest.fn(), + }; + + const fnMigrateFrom = { + name: 'fnMigrateFrom', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + migrations: { + '8.1.0': ((state: ExpressionAstFunction, version: string) => { + const migrateToAst = parseExpression('fnMigrateTo'); + const { arguments: args } = state; + const ast = { ...migrateToAst.chain[0], arguments: args }; + return { type: 'expression', chain: [ast, ast] }; + }) as unknown as MigrateFunction, + }, + fn: jest.fn(), + }; + executor.registerFunction(fnMigrateFrom); + executor.registerFunction(fnMigrateTo); + test('calls migrate function for every expression function in expression', () => { executor.migrateToLatest({ state: parseExpression( @@ -255,6 +289,25 @@ describe('Executor', () => { }); expect(migrateFn).toBeCalledTimes(5); }); + + test('migrates expression function to expression function or chain of expression functions', () => { + const plainExpression = 'foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'; + const plainExpressionAst = parseExpression(plainExpression); + const migratedExpressionAst = executor.migrateToLatest({ + state: parseExpression(`${plainExpression} | fnMigrateFrom bar="baz" | fnMigrateTo`), + version: '8.0.0', + }); + + expect(migratedExpressionAst).toEqual({ + type: 'expression', + chain: [ + ...plainExpressionAst.chain, + { type: 'function', function: 'fnMigrateTo', arguments: { bar: ['baz'] } }, + { type: 'function', function: 'fnMigrateTo', arguments: { bar: ['baz'] } }, + { type: 'function', function: 'fnMigrateTo', arguments: {} }, + ], + }); + }); }); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 01b54d13f8a76..86516344031a0 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -241,6 +241,61 @@ export class Executor = Record ExpressionAstFunction | ExpressionAstExpression + ): ExpressionAstExpression { + let additionalFunctions = 0; + return ( + ast.chain.reduce( + (newAst: ExpressionAstExpression, funcAst: ExpressionAstFunction, index: number) => { + const realIndex = index + additionalFunctions; + const { function: fnName, arguments: fnArgs } = funcAst; + const fn = getByAlias(this.getFunctions(), fnName); + if (!fn) { + return newAst; + } + + // if any of arguments are expressions we should migrate those first + funcAst.arguments = mapValues(fnArgs, (asts) => + asts.map((arg) => + arg != null && typeof arg === 'object' + ? this.walkAstAndTransform(arg, transform) + : arg + ) + ); + + const transformedFn = transform(fn, funcAst); + if (transformedFn.type === 'function') { + const prevChain = realIndex > 0 ? newAst.chain.slice(0, realIndex) : []; + const nextChain = newAst.chain.slice(realIndex + 1); + return { + ...newAst, + chain: [...prevChain, transformedFn, ...nextChain], + }; + } + + if (transformedFn.type === 'expression') { + const { chain } = transformedFn; + const prevChain = realIndex > 0 ? newAst.chain.slice(0, realIndex) : []; + const nextChain = newAst.chain.slice(realIndex + 1); + additionalFunctions += chain.length - 1; + return { + ...newAst, + chain: [...prevChain, ...chain, ...nextChain], + }; + } + + return newAst; + }, + ast + ) ?? ast + ); + } + public inject(ast: ExpressionAstExpression, references: SavedObjectReference[]) { let linkId = 0; return this.walkAst(cloneDeep(ast), (fn, link) => { @@ -296,14 +351,12 @@ export class Executor = Record { + return this.walkAstAndTransform(cloneDeep(ast) as ExpressionAstExpression, (fn, link) => { if (!fn.migrations[version]) { - return; + return link; } - ({ arguments: link.arguments, type: link.type } = fn.migrations[version]( - link - ) as ExpressionAstFunction); + return fn.migrations[version](link) as ExpressionAstExpression; }); } diff --git a/src/plugins/expressions/common/expression_types/specs/filter.ts b/src/plugins/expressions/common/expression_types/specs/filter.ts index dad69f9433a23..915beceb988fd 100644 --- a/src/plugins/expressions/common/expression_types/specs/filter.ts +++ b/src/plugins/expressions/common/expression_types/specs/filter.ts @@ -15,6 +15,7 @@ export type ExpressionValueFilter = ExpressionValueBoxed< 'filter', { filterType?: string; + filterGroup?: string; value?: string; column?: string; and: ExpressionValueFilter[]; diff --git a/src/plugins/home/server/services/sample_data/errors.ts b/src/plugins/home/server/services/sample_data/errors.ts new file mode 100644 index 0000000000000..832c520b9ade8 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/errors.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class SampleDataInstallError extends Error { + constructor(message: string, public readonly httpCode: number) { + super(message); + } +} diff --git a/src/plugins/home/server/services/sample_data/lib/insert_data_into_index.ts b/src/plugins/home/server/services/sample_data/lib/insert_data_into_index.ts new file mode 100644 index 0000000000000..4a7d7e9813dcc --- /dev/null +++ b/src/plugins/home/server/services/sample_data/lib/insert_data_into_index.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IScopedClusterClient, Logger } from 'kibana/server'; +import type { DataIndexSchema } from './sample_dataset_registry_types'; +import { + translateTimeRelativeToDifference, + translateTimeRelativeToWeek, +} from './translate_timestamp'; +import { loadData } from './load_data'; + +export const insertDataIntoIndex = ({ + dataIndexConfig, + logger, + esClient, + index, + nowReference, +}: { + dataIndexConfig: DataIndexSchema; + index: string; + nowReference: string; + esClient: IScopedClusterClient; + logger: Logger; +}) => { + const updateTimestamps = (doc: any) => { + dataIndexConfig.timeFields + .filter((timeFieldName: string) => doc[timeFieldName]) + .forEach((timeFieldName: string) => { + doc[timeFieldName] = dataIndexConfig.preserveDayOfWeekTimeOfDay + ? translateTimeRelativeToWeek( + doc[timeFieldName], + dataIndexConfig.currentTimeMarker, + nowReference + ) + : translateTimeRelativeToDifference( + doc[timeFieldName], + dataIndexConfig.currentTimeMarker, + nowReference + ); + }); + return doc; + }; + + const bulkInsert = async (docs: unknown[]) => { + const insertCmd = { index: { _index: index } }; + const bulk: unknown[] = []; + docs.forEach((doc: unknown) => { + bulk.push(insertCmd); + bulk.push(updateTimestamps(doc)); + }); + + const { body: resp } = await esClient.asCurrentUser.bulk({ + body: bulk, + }); + + if (resp.errors) { + const errMsg = `sample_data install errors while bulk inserting. Elasticsearch response: ${JSON.stringify( + resp, + null, + '' + )}`; + logger.warn(errMsg); + return Promise.reject( + new Error(`Unable to load sample data into index "${index}", see kibana logs for details`) + ); + } + }; + return loadData(dataIndexConfig.dataPath, bulkInsert); // this returns a Promise +}; diff --git a/src/plugins/home/server/services/sample_data/lib/load_data.ts b/src/plugins/home/server/services/sample_data/lib/load_data.ts index 4d203f791da97..b039243b0cc25 100644 --- a/src/plugins/home/server/services/sample_data/lib/load_data.ts +++ b/src/plugins/home/server/services/sample_data/lib/load_data.ts @@ -12,7 +12,10 @@ import { createUnzip } from 'zlib'; const BULK_INSERT_SIZE = 500; -export function loadData(path: any, bulkInsert: (docs: any[]) => Promise) { +export function loadData( + path: string, + bulkInsert: (docs: unknown[]) => Promise +): Promise { return new Promise((resolve, reject) => { let count: number = 0; let docs: any[] = []; diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index 17d35c6cb4b7e..21c77ec51e5ef 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -6,73 +6,12 @@ * Side Public License, v 1. */ -import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; -import { IRouter, Logger, IScopedClusterClient } from 'src/core/server'; +import { IRouter, Logger } from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; -import { createIndexName } from '../lib/create_index_name'; -import { - dateToIso8601IgnoringTime, - translateTimeRelativeToDifference, - translateTimeRelativeToWeek, -} from '../lib/translate_timestamp'; -import { loadData } from '../lib/load_data'; import { SampleDataUsageTracker } from '../usage/usage'; -import { getSavedObjectsClient } from './utils'; -import { getUniqueObjectTypes } from '../lib/utils'; - -const insertDataIntoIndex = ( - dataIndexConfig: any, - index: string, - nowReference: string, - esClient: IScopedClusterClient, - logger: Logger -) => { - function updateTimestamps(doc: any) { - dataIndexConfig.timeFields - .filter((timeFieldName: string) => doc[timeFieldName]) - .forEach((timeFieldName: string) => { - doc[timeFieldName] = dataIndexConfig.preserveDayOfWeekTimeOfDay - ? translateTimeRelativeToWeek( - doc[timeFieldName], - dataIndexConfig.currentTimeMarker, - nowReference - ) - : translateTimeRelativeToDifference( - doc[timeFieldName], - dataIndexConfig.currentTimeMarker, - nowReference - ); - }); - return doc; - } - - const bulkInsert = async (docs: any) => { - const insertCmd = { index: { _index: index } }; - const bulk: any[] = []; - docs.forEach((doc: any) => { - bulk.push(insertCmd); - bulk.push(updateTimestamps(doc)); - }); - - const { body: resp } = await esClient.asCurrentUser.bulk({ - body: bulk, - }); - - if (resp.errors) { - const errMsg = `sample_data install errors while bulk inserting. Elasticsearch response: ${JSON.stringify( - resp, - null, - '' - )}`; - logger.warn(errMsg); - return Promise.reject( - new Error(`Unable to load sample data into index "${index}", see kibana logs for details`) - ); - } - }; - return loadData(dataIndexConfig.dataPath, bulkInsert); // this returns a Promise -}; +import { getSampleDataInstaller } from './utils'; +import { SampleDataInstallError } from '../errors'; export function createInstallRoute( router: IRouter, @@ -95,86 +34,38 @@ export function createInstallRoute( if (!sampleDataset) { return res.notFound(); } + // @ts-ignore Custom query validation used const now = query.now ? new Date(query.now) : new Date(); - const nowReference = dateToIso8601IgnoringTime(now); - const counts = {}; - for (let i = 0; i < sampleDataset.dataIndices.length; i++) { - const dataIndexConfig = sampleDataset.dataIndices[i]; - const index = createIndexName(sampleDataset.id, dataIndexConfig.id); - // clean up any old installation of dataset - try { - await context.core.elasticsearch.client.asCurrentUser.indices.delete({ - index, - }); - } catch (err) { - // ignore delete errors - } + const sampleDataInstaller = getSampleDataInstaller({ + datasetId: sampleDataset.id, + sampleDatasets, + logger, + context, + }); - try { - await context.core.elasticsearch.client.asCurrentUser.indices.create({ - index, + try { + const installResult = await sampleDataInstaller.install(params.id, now); + // track the usage operation in a non-blocking way + usageTracker.addInstall(params.id); + return res.ok({ + body: { + elasticsearchIndicesCreated: installResult.createdDocsPerIndex, + kibanaSavedObjectsLoaded: installResult.createdSavedObjects, + }, + }); + } catch (e) { + if (e instanceof SampleDataInstallError) { + return res.customError({ body: { - settings: { index: { number_of_shards: 1, auto_expand_replicas: '0-1' } }, - mappings: { properties: dataIndexConfig.fields }, + message: e.message, }, + statusCode: e.httpCode, }); - } catch (err) { - const errMsg = `Unable to create sample data index "${index}", error: ${err.message}`; - logger.warn(errMsg); - return res.customError({ body: errMsg, statusCode: err.status }); - } - - try { - const count = await insertDataIntoIndex( - dataIndexConfig, - index, - nowReference, - context.core.elasticsearch.client, - logger - ); - (counts as any)[index] = count; - } catch (err) { - const errMsg = `sample_data install errors while loading data. Error: ${err}`; - throw new Error(errMsg); } + throw e; } - - const { getImporter } = context.core.savedObjects; - const objectTypes = getUniqueObjectTypes(sampleDataset.savedObjects); - const savedObjectsClient = getSavedObjectsClient(context, objectTypes); - const importer = getImporter(savedObjectsClient); - - const savedObjects = sampleDataset.savedObjects.map(({ version, ...obj }) => obj); - const readStream = Readable.from(savedObjects); - - try { - const { errors = [] } = await importer.import({ - readStream, - overwrite: true, - createNewCopies: false, - }); - if (errors.length > 0) { - const errMsg = `sample_data install errors while loading saved objects. Errors: ${JSON.stringify( - errors.map(({ type, id, error }) => ({ type, id, error })) // discard other fields - )}`; - logger.warn(errMsg); - return res.customError({ body: errMsg, statusCode: 500 }); - } - } catch (err) { - const errMsg = `import failed, error: ${err.message}`; - throw new Error(errMsg); - } - usageTracker.addInstall(params.id); - - // FINALLY - return res.ok({ - body: { - elasticsearchIndicesCreated: counts, - kibanaSavedObjectsLoaded: sampleDataset.savedObjects.length, - }, - }); } ); } diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index b0e8e6f102f1e..52f725da4906b 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -6,15 +6,12 @@ * Side Public License, v 1. */ -import { isBoom } from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import type { IRouter, Logger } from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; -import { createIndexName } from '../lib/create_index_name'; import { SampleDataUsageTracker } from '../usage/usage'; -import { findSampleObjects } from '../lib/find_sample_objects'; -import { getUniqueObjectTypes } from '../lib/utils'; -import { getSavedObjectsClient } from './utils'; +import { getSampleDataInstaller } from './utils'; +import { SampleDataInstallError } from '../errors'; export function createUninstallRoute( router: IRouter, @@ -31,62 +28,33 @@ export function createUninstallRoute( }, async (context, request, response) => { const sampleDataset = sampleDatasets.find(({ id }) => id === request.params.id); - if (!sampleDataset) { return response.notFound(); } - for (let i = 0; i < sampleDataset.dataIndices.length; i++) { - const dataIndexConfig = sampleDataset.dataIndices[i]; - const index = createIndexName(sampleDataset.id, dataIndexConfig.id); - - try { - // TODO: don't delete the index if sample data exists in other spaces (#116677) - await context.core.elasticsearch.client.asCurrentUser.indices.delete({ index }); - } catch (err) { - // if the index doesn't exist, ignore the error and proceed - if (err.body.status !== 404) { - return response.customError({ - statusCode: err.body.status, - body: { - message: `Unable to delete sample data index "${index}", error: ${err.body.error.type}`, - }, - }); - } - } - } - - const objects = sampleDataset.savedObjects.map(({ type, id }) => ({ type, id })); - const objectTypes = getUniqueObjectTypes(objects); - const client = getSavedObjectsClient(context, objectTypes); - const findSampleObjectsResult = await findSampleObjects({ client, logger, objects }); - - const objectsToDelete = findSampleObjectsResult.filter(({ foundObjectId }) => foundObjectId); - const deletePromises = objectsToDelete.map(({ type, foundObjectId }) => - client.delete(type, foundObjectId!).catch((err) => { - // if the object doesn't exist, ignore the error and proceed - if (isBoom(err) && err.output.statusCode === 404) { - return; - } - throw err; - }) - ); + const sampleDataInstaller = getSampleDataInstaller({ + datasetId: sampleDataset.id, + sampleDatasets, + logger, + context, + }); try { - await Promise.all(deletePromises); - } catch (err) { - return response.customError({ - statusCode: err.body.status, - body: { - message: `Unable to delete sample dataset saved objects, error: ${err.body.error.type}`, - }, - }); + await sampleDataInstaller.uninstall(request.params.id); + // track the usage operation in a non-blocking way + usageTracker.addUninstall(request.params.id); + return response.noContent(); + } catch (e) { + if (e instanceof SampleDataInstallError) { + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.httpCode, + }); + } + throw e; } - - // track the usage operation in a non-blocking way - usageTracker.addUninstall(request.params.id); - - return response.noContent(); } ); } diff --git a/src/plugins/home/server/services/sample_data/routes/utils.ts b/src/plugins/home/server/services/sample_data/routes/utils.ts index 6bab00895440a..36b5534d9f4af 100644 --- a/src/plugins/home/server/services/sample_data/routes/utils.ts +++ b/src/plugins/home/server/services/sample_data/routes/utils.ts @@ -6,12 +6,41 @@ * Side Public License, v 1. */ -import type { RequestHandlerContext } from 'src/core/server'; +import type { RequestHandlerContext, Logger } from 'src/core/server'; +import type { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; +import { SampleDataInstaller } from '../sample_data_installer'; +import { getUniqueObjectTypes } from '../lib/utils'; -export function getSavedObjectsClient(context: RequestHandlerContext, objectTypes: string[]) { +export const getSampleDataInstaller = ({ + datasetId, + context, + sampleDatasets, + logger, +}: { + datasetId: string; + context: RequestHandlerContext; + sampleDatasets: SampleDatasetSchema[]; + logger: Logger; +}) => { + const sampleDataset = sampleDatasets.find(({ id }) => id === datasetId)!; + const { getImporter, client: soClient } = context.core.savedObjects; + const objectTypes = getUniqueObjectTypes(sampleDataset.savedObjects); + const savedObjectsClient = getSavedObjectsClient(context, objectTypes); + const soImporter = getImporter(savedObjectsClient); + + return new SampleDataInstaller({ + esClient: context.core.elasticsearch.client, + soImporter, + soClient, + logger, + sampleDatasets, + }); +}; + +export const getSavedObjectsClient = (context: RequestHandlerContext, objectTypes: string[]) => { const { getClient, typeRegistry } = context.core.savedObjects; const includedHiddenTypes = objectTypes.filter((supportedType) => typeRegistry.isHidden(supportedType) ); return getClient({ includedHiddenTypes }); -} +}; diff --git a/src/plugins/home/server/services/sample_data/sample_data_installer.test.mocks.ts b/src/plugins/home/server/services/sample_data/sample_data_installer.test.mocks.ts new file mode 100644 index 0000000000000..c8bdf0cc692b8 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/sample_data_installer.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const insertDataIntoIndexMock = jest.fn(); +jest.doMock('./lib/insert_data_into_index', () => ({ + insertDataIntoIndex: insertDataIntoIndexMock, +})); + +export const findSampleObjectsMock = jest.fn(); +jest.doMock('./lib/find_sample_objects', () => ({ + findSampleObjects: findSampleObjectsMock, +})); diff --git a/src/plugins/home/server/services/sample_data/sample_data_installer.test.ts b/src/plugins/home/server/services/sample_data/sample_data_installer.test.ts new file mode 100644 index 0000000000000..22079cbcafdb3 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/sample_data_installer.test.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Readable } from 'stream'; +import { insertDataIntoIndexMock, findSampleObjectsMock } from './sample_data_installer.test.mocks'; +import type { SavedObjectsImportFailure } from 'kibana/server'; +import { + savedObjectsClientMock, + savedObjectsServiceMock, + elasticsearchServiceMock, + loggingSystemMock, +} from '../../../../../core/server/mocks'; +import type { SampleDatasetSchema } from './lib/sample_dataset_registry_types'; +import { SampleDataInstaller } from './sample_data_installer'; +import { SampleDataInstallError } from './errors'; + +const testDatasets: SampleDatasetSchema[] = [ + { + id: 'test_single_data_index', + name: 'Test with a single data index', + description: 'See name', + previewImagePath: 'previewImagePath', + darkPreviewImagePath: 'darkPreviewImagePath', + overviewDashboard: 'overviewDashboard', + defaultIndex: 'defaultIndex', + savedObjects: [ + { + id: 'some-dashboard', + type: 'dashboard', + attributes: { + hello: 'dolly', + }, + references: [], + }, + { + id: 'another-dashboard', + type: 'dashboard', + attributes: { + foo: 'bar', + }, + references: [], + }, + ], + dataIndices: [ + { + id: 'test_single_data_index', + dataPath: '/dataPath', + fields: { someField: { type: 'keyword' } }, + currentTimeMarker: '2018-01-09T00:00:00', + timeFields: ['@timestamp'], + preserveDayOfWeekTimeOfDay: true, + }, + ], + }, +]; + +describe('SampleDataInstaller', () => { + let esClient: ReturnType; + let soClient: ReturnType; + let soImporter: ReturnType; + let logger: ReturnType; + let installer: SampleDataInstaller; + + beforeEach(() => { + esClient = elasticsearchServiceMock.createScopedClusterClient(); + soClient = savedObjectsClientMock.create(); + soImporter = savedObjectsServiceMock.createImporter(); + logger = loggingSystemMock.createLogger(); + + installer = new SampleDataInstaller({ + esClient, + soClient, + soImporter, + logger, + sampleDatasets: testDatasets, + }); + + soImporter.import.mockResolvedValue({ + success: true, + successCount: 1, + errors: [], + warnings: [], + }); + + soClient.delete.mockResolvedValue({}); + + esClient.asCurrentUser.indices.getAlias.mockImplementation(() => { + throw new Error('alias not found'); + }); + + findSampleObjectsMock.mockResolvedValue([]); + }); + + afterEach(() => { + insertDataIntoIndexMock.mockReset(); + findSampleObjectsMock.mockReset(); + }); + + describe('#install', () => { + it('cleanups the data index before installing', async () => { + await installer.install('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + }); + }); + + it('creates the data index', async () => { + await installer.install('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.create).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.create).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + body: { + settings: { index: { number_of_shards: 1, auto_expand_replicas: '0-1' } }, + mappings: { properties: { someField: { type: 'keyword' } } }, + }, + }); + }); + + it('inserts the data into the index', async () => { + await installer.install('test_single_data_index'); + + expect(insertDataIntoIndexMock).toHaveBeenCalledTimes(1); + expect(insertDataIntoIndexMock).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + nowReference: expect.any(String), + logger, + esClient, + dataIndexConfig: testDatasets[0].dataIndices[0], + }); + }); + + it('imports the saved objects', async () => { + await installer.install('test_single_data_index'); + + expect(soImporter.import).toHaveBeenCalledTimes(1); + expect(soImporter.import).toHaveBeenCalledWith({ + readStream: expect.any(Readable), + overwrite: true, + createNewCopies: false, + }); + }); + + it('throws a SampleDataInstallError with code 404 when the dataset is not found', async () => { + try { + await installer.install('unknown_data_set'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(404); + } + }); + + it('does not throw when the index removal fails', async () => { + esClient.asCurrentUser.indices.delete.mockImplementation(() => { + throw new Error('cannot delete index'); + }); + + await expect(installer.install('test_single_data_index')).resolves.toBeDefined(); + }); + + it('throws a SampleDataInstallError when the index creation fails', async () => { + esClient.asCurrentUser.indices.create.mockImplementation(() => { + // eslint-disable-next-line no-throw-literal + throw { + message: 'Cannot create index', + status: 500, + }; + }); + + try { + await installer.install('test_single_data_index'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(500); + } + }); + + it('throws a SampleDataInstallError if the savedObject import returns any error', async () => { + soImporter.import.mockResolvedValue({ + success: true, + successCount: 1, + errors: [{ type: 'type', id: 'id' } as SavedObjectsImportFailure], + warnings: [], + }); + + try { + await installer.install('test_single_data_index'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect(e.message).toContain('sample_data install errors while loading saved objects'); + expect((e as SampleDataInstallError).httpCode).toEqual(500); + } + }); + + describe('when the data index is using an alias', () => { + it('deletes the alias and the index', async () => { + const indexName = 'target_index'; + + esClient.asCurrentUser.indices.getAlias.mockResolvedValue( + elasticsearchServiceMock.createApiResponse({ + body: { + [indexName]: { + aliases: { + kibana_sample_data_test_single_data_index: {}, + }, + }, + }, + }) + ); + + await installer.install('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledWith({ + name: 'kibana_sample_data_test_single_data_index', + index: indexName, + }); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: indexName, + }); + }); + }); + }); + + describe('#uninstall', () => { + it('deletes the data index', async () => { + await installer.uninstall('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: 'kibana_sample_data_test_single_data_index', + }); + }); + + it('deletes the saved objects', async () => { + findSampleObjectsMock.mockResolvedValue([ + { type: 'dashboard', id: 'foo', foundObjectId: 'foo' }, + { type: 'dashboard', id: 'hello', foundObjectId: 'dolly' }, + ]); + + await installer.uninstall('test_single_data_index'); + + expect(soClient.delete).toHaveBeenCalledTimes(2); + expect(soClient.delete).toHaveBeenCalledWith('dashboard', 'foo'); + expect(soClient.delete).toHaveBeenCalledWith('dashboard', 'dolly'); + }); + + it('throws a SampleDataInstallError with code 404 when the dataset is not found', async () => { + try { + await installer.uninstall('unknown_data_set'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(404); + } + }); + + it('does not throw when the index removal fails', async () => { + esClient.asCurrentUser.indices.delete.mockImplementation(() => { + throw new Error('cannot delete index'); + }); + + await expect(installer.uninstall('test_single_data_index')).resolves.toBeDefined(); + }); + + it('throws a SampleDataInstallError if any SO deletion fails', async () => { + findSampleObjectsMock.mockResolvedValue([ + { type: 'dashboard', id: 'foo', foundObjectId: 'foo' }, + { type: 'dashboard', id: 'hello', foundObjectId: 'dolly' }, + ]); + + soClient.delete.mockImplementation(async (type: string, id: string) => { + if (id === 'dolly') { + throw new Error('could not delete dolly'); + } + return {}; + }); + + try { + await installer.uninstall('test_single_data_index'); + expect('should have returned an error').toEqual('but it did not'); + } catch (e) { + expect(e).toBeInstanceOf(SampleDataInstallError); + expect((e as SampleDataInstallError).httpCode).toEqual(500); + } + }); + + describe('when the data index is using an alias', () => { + it('deletes the alias and the index', async () => { + const indexName = 'target_index'; + + esClient.asCurrentUser.indices.getAlias.mockResolvedValue( + elasticsearchServiceMock.createApiResponse({ + body: { + [indexName]: { + aliases: { + kibana_sample_data_test_single_data_index: {}, + }, + }, + }, + }) + ); + + await installer.uninstall('test_single_data_index'); + + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.deleteAlias).toHaveBeenCalledWith({ + name: 'kibana_sample_data_test_single_data_index', + index: indexName, + }); + + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.asCurrentUser.indices.delete).toHaveBeenCalledWith({ + index: indexName, + }); + }); + }); + }); +}); diff --git a/src/plugins/home/server/services/sample_data/sample_data_installer.ts b/src/plugins/home/server/services/sample_data/sample_data_installer.ts new file mode 100644 index 0000000000000..8e9315719bc16 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/sample_data_installer.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Readable } from 'stream'; +import { isBoom } from '@hapi/boom'; +import type { + IScopedClusterClient, + ISavedObjectsImporter, + Logger, + SavedObjectsClientContract, +} from 'src/core/server'; +import type { SampleDatasetSchema, DataIndexSchema } from './lib/sample_dataset_registry_types'; +import { dateToIso8601IgnoringTime } from './lib/translate_timestamp'; +import { createIndexName } from './lib/create_index_name'; +import { insertDataIntoIndex } from './lib/insert_data_into_index'; +import { SampleDataInstallError } from './errors'; +import { findSampleObjects } from './lib/find_sample_objects'; + +export interface SampleDataInstallerOptions { + esClient: IScopedClusterClient; + soClient: SavedObjectsClientContract; + soImporter: ISavedObjectsImporter; + sampleDatasets: SampleDatasetSchema[]; + logger: Logger; +} + +export interface SampleDataInstallResult { + createdDocsPerIndex: Record; + createdSavedObjects: number; +} + +/** + * Utility class in charge of installing and uninstalling sample datasets + */ +export class SampleDataInstaller { + private readonly esClient: IScopedClusterClient; + private readonly soClient: SavedObjectsClientContract; + private readonly soImporter: ISavedObjectsImporter; + private readonly sampleDatasets: SampleDatasetSchema[]; + private readonly logger: Logger; + + constructor({ + esClient, + soImporter, + soClient, + sampleDatasets, + logger, + }: SampleDataInstallerOptions) { + this.esClient = esClient; + this.soClient = soClient; + this.soImporter = soImporter; + this.sampleDatasets = sampleDatasets; + this.logger = logger; + } + + async install( + datasetId: string, + installDate: Date = new Date() + ): Promise { + const sampleDataset = this.sampleDatasets.find(({ id }) => id === datasetId); + if (!sampleDataset) { + throw new SampleDataInstallError(`Sample dataset ${datasetId} not found`, 404); + } + + const nowReference = dateToIso8601IgnoringTime(installDate); + const createdDocsPerIndex: Record = {}; + + for (let i = 0; i < sampleDataset.dataIndices.length; i++) { + const dataIndex = sampleDataset.dataIndices[i]; + const indexName = createIndexName(sampleDataset.id, dataIndex.id); + // clean up any old installation of dataset + await this.uninstallDataIndex(sampleDataset, dataIndex); + await this.installDataIndex(sampleDataset, dataIndex); + + const injectedCount = await insertDataIntoIndex({ + index: indexName, + nowReference, + logger: this.logger, + esClient: this.esClient, + dataIndexConfig: dataIndex, + }); + createdDocsPerIndex[indexName] = injectedCount; + } + + const createdSavedObjects = await this.importSavedObjects(sampleDataset); + + return { + createdDocsPerIndex, + createdSavedObjects, + }; + } + + async uninstall(datasetId: string) { + const sampleDataset = this.sampleDatasets.find(({ id }) => id === datasetId); + if (!sampleDataset) { + throw new SampleDataInstallError(`Sample dataset ${datasetId} not found`, 404); + } + + for (let i = 0; i < sampleDataset.dataIndices.length; i++) { + const dataIndex = sampleDataset.dataIndices[i]; + await this.uninstallDataIndex(sampleDataset, dataIndex); + } + const deletedObjects = await this.deleteSavedObjects(sampleDataset); + + return { + deletedSavedObjects: deletedObjects, + }; + } + + private async uninstallDataIndex(dataset: SampleDatasetSchema, dataIndex: DataIndexSchema) { + let index = createIndexName(dataset.id, dataIndex.id); + + try { + // if the sample data was reindexed using UA, the index name is actually an alias pointing to the reindexed + // index. In that case, we need to get rid of the alias and to delete the underlying index + const { body: response } = await this.esClient.asCurrentUser.indices.getAlias({ + name: index, + }); + const aliasName = index; + index = Object.keys(response)[0]; + await this.esClient.asCurrentUser.indices.deleteAlias({ name: aliasName, index }); + } catch (err) { + // ignore errors from missing alias + } + + try { + await this.esClient.asCurrentUser.indices.delete({ + index, + }); + } catch (err) { + // ignore delete errors + } + } + + private async installDataIndex(dataset: SampleDatasetSchema, dataIndex: DataIndexSchema) { + const index = createIndexName(dataset.id, dataIndex.id); + try { + await this.esClient.asCurrentUser.indices.create({ + index, + body: { + settings: { index: { number_of_shards: 1, auto_expand_replicas: '0-1' } }, + mappings: { properties: dataIndex.fields }, + }, + }); + } catch (err) { + const errMsg = `Unable to create sample data index "${index}", error: ${err.message}`; + this.logger.warn(errMsg); + throw new SampleDataInstallError(errMsg, err.status); + } + } + + private async importSavedObjects(dataset: SampleDatasetSchema) { + const savedObjects = dataset.savedObjects.map(({ version, ...obj }) => obj); + const readStream = Readable.from(savedObjects); + + const { errors = [] } = await this.soImporter.import({ + readStream, + overwrite: true, + createNewCopies: false, + }); + if (errors.length > 0) { + const errMsg = `sample_data install errors while loading saved objects. Errors: ${JSON.stringify( + errors.map(({ type, id, error }) => ({ type, id, error })) // discard other fields + )}`; + this.logger.warn(errMsg); + throw new SampleDataInstallError(errMsg, 500); + } + return savedObjects.length; + } + + private async deleteSavedObjects(dataset: SampleDatasetSchema) { + const objects = dataset.savedObjects.map(({ type, id }) => ({ type, id })); + const findSampleObjectsResult = await findSampleObjects({ + client: this.soClient, + logger: this.logger, + objects, + }); + const objectsToDelete = findSampleObjectsResult.filter(({ foundObjectId }) => foundObjectId); + const deletePromises = objectsToDelete.map(({ type, foundObjectId }) => + this.soClient.delete(type, foundObjectId!).catch((err) => { + // if the object doesn't exist, ignore the error and proceed + if (isBoom(err) && err.output.statusCode === 404) { + return; + } + throw err; + }) + ); + try { + await Promise.all(deletePromises); + } catch (err) { + throw new SampleDataInstallError( + `Unable to delete sample dataset saved objects, error: ${ + err.body?.error?.type ?? err.message + }`, + err.body?.status ?? 500 + ); + } + return objectsToDelete.length; + } +} diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts new file mode 100644 index 0000000000000..29702c3356865 --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DependencyManager } from './dependency_manager'; + +describe('DependencyManager', () => { + it('orderDependencies. Should sort topology by dependencies', () => { + const graph = { + N: [], + R: [], + A: ['B', 'C'], + B: ['D'], + C: ['F', 'B'], + F: ['E'], + E: ['D'], + D: ['L'], + }; + const sortedTopology = ['N', 'R', 'L', 'D', 'B', 'E', 'F', 'C', 'A']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + + it('orderDependencies. Should return base topology if no depended vertices', () => { + const graph = { + N: [], + R: [], + D: undefined, + }; + const sortedTopology = ['N', 'R', 'D']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + + it('orderDependencies. Should detect circular dependencies and throw error with path', () => { + const graph = { + N: ['R'], + R: ['A'], + A: ['B'], + B: ['C'], + C: ['D'], + D: ['E'], + E: ['F'], + F: ['L'], + L: ['G'], + G: ['N'], + }; + const circularPath = ['N', 'R', 'A', 'B', 'C', 'D', 'E', 'F', 'L', 'G', 'N'].join(' -> '); + const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + + expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + }); +}); diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.ts new file mode 100644 index 0000000000000..de30b180607fe --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +type GraphVertex = string | number | symbol; +type Graph = Record; +type BreadCrumbs = Record; + +interface CycleDetectionResult { + hasCycle: boolean; + path: T[]; +} + +export class DependencyManager { + static orderDependencies(graph: Graph) { + const cycleInfo = DependencyManager.getSortedDependencies(graph); + if (cycleInfo.hasCycle) { + const error = DependencyManager.getCyclePathError(cycleInfo.path); + DependencyManager.throwCyclicPathError(error); + } + + return cycleInfo.path; + } + + /** + * DFS algorithm for checking if graph is a DAG (Directed Acyclic Graph) + * and sorting topogy (dependencies) if graph is DAG. + * @param {Graph} graph - graph of dependencies. + */ + private static getSortedDependencies( + graph: Graph = {} as Graph + ): CycleDetectionResult { + const sortedVertices: Set = new Set(); + const vertices = Object.keys(graph) as T[]; + return vertices.reduce>((cycleInfo, srcVertex) => { + if (cycleInfo.hasCycle) { + return cycleInfo; + } + + return DependencyManager.sortVerticesFrom(srcVertex, graph, sortedVertices, {}, {}); + }, DependencyManager.createCycleInfo()); + } + + /** + * Modified DFS algorithm for topological sort. + * @param {T extends GraphVertex} srcVertex - a source vertex - the start point of dependencies ordering. + * @param {Graph} graph - graph of dependencies, represented in the adjacency list form. + * @param {Set} sortedVertices - ordered dependencies path from the free to the dependent vertex. + * @param {BreadCrumbs} visited - record of visited vertices. + * @param {BreadCrumbs} inpath - record of vertices, which was met in the path. Is used for detecting cycles. + */ + private static sortVerticesFrom( + srcVertex: T, + graph: Graph, + sortedVertices: Set, + visited: BreadCrumbs = {}, + inpath: BreadCrumbs = {} + ): CycleDetectionResult { + visited[srcVertex] = true; + inpath[srcVertex] = true; + const cycleInfo = graph[srcVertex]?.reduce | undefined>( + (info, vertex) => { + if (inpath[vertex]) { + const path = (Object.keys(inpath) as T[]).filter( + (visitedVertex) => inpath[visitedVertex] + ); + return DependencyManager.createCycleInfo([...path, vertex], true); + } else if (!visited[vertex]) { + return DependencyManager.sortVerticesFrom(vertex, graph, sortedVertices, visited, inpath); + } + return info; + }, + undefined + ); + + inpath[srcVertex] = false; + + if (!sortedVertices.has(srcVertex)) { + sortedVertices.add(srcVertex); + } + + return cycleInfo ?? DependencyManager.createCycleInfo([...sortedVertices]); + } + + private static createCycleInfo( + path: T[] = [], + hasCycle: boolean = false + ): CycleDetectionResult { + return { hasCycle, path }; + } + + private static getCyclePathError( + cyclePath: CycleDetectionResult['path'] + ) { + const cycleString = cyclePath.join(' -> '); + return `Circular dependency detected while setting up services: ${cycleString}`; + } + + private static throwCyclicPathError(error: string) { + throw new Error(error); + } +} diff --git a/src/plugins/presentation_util/public/services/create/factory.ts b/src/plugins/presentation_util/public/services/create/factory.ts index ddc2e5845b037..49ed5ef8aaf8d 100644 --- a/src/plugins/presentation_util/public/services/create/factory.ts +++ b/src/plugins/presentation_util/public/services/create/factory.ts @@ -16,7 +16,10 @@ import { CoreStart, AppUpdater, PluginInitializerContext } from 'src/core/public * The `StartParameters` generic determines what parameters are expected to * create the service. */ -export type PluginServiceFactory = (params: Parameters) => Service; +export type PluginServiceFactory = ( + params: Parameters, + requiredServices: RequiredServices +) => Service; /** * Parameters necessary to create a Kibana-based service, (e.g. during Plugin @@ -38,6 +41,7 @@ export interface KibanaPluginServiceParams { * The `Setup` generic refers to the specific Plugin `TPluginsSetup`. * The `Start` generic refers to the specific Plugin `TPluginsStart`. */ -export type KibanaPluginServiceFactory = ( - params: KibanaPluginServiceParams +export type KibanaPluginServiceFactory = ( + params: KibanaPluginServiceParams, + requiredServices: RequiredServices ) => Service; diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx index 06590bcfbb3d0..3271dc52fd9d0 100644 --- a/src/plugins/presentation_util/public/services/create/provider.tsx +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -17,7 +17,25 @@ import { PluginServiceFactory } from './factory'; * start the service. */ export type PluginServiceProviders = { - [K in keyof Services]: PluginServiceProvider; + [K in keyof Services]: PluginServiceProvider< + Services[K], + StartParameters, + Services, + Array + >; +}; + +type ElementOfArray = ArrayType extends Array< + infer ElementType +> + ? ElementType + : never; + +export type PluginServiceRequiredServices< + RequiredServices extends Array, + AvailableServices +> = { + [K in ElementOfArray]: AvailableServices[K]; }; /** @@ -27,16 +45,34 @@ export type PluginServiceProviders = { * The `StartParameters` generic determines what parameters are expected to * start the service. */ -export class PluginServiceProvider { - private factory: PluginServiceFactory; +export class PluginServiceProvider< + Service extends {}, + StartParameters = {}, + Services = {}, + RequiredServices extends Array = [] +> { + private factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >; + private _requiredServices?: RequiredServices; private context = createContext(null); private pluginService: Service | null = null; public readonly Provider: React.FC = ({ children }) => { return {children}; }; - constructor(factory: PluginServiceFactory) { + constructor( + factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >, + requiredServices?: RequiredServices + ) { this.factory = factory; + this._requiredServices = requiredServices; this.context.displayName = 'PluginServiceContext'; } @@ -55,8 +91,11 @@ export class PluginServiceProvider { * * @param params Parameters used to start the service. */ - start(params: StartParameters) { - this.pluginService = this.factory(params); + start( + params: StartParameters, + requiredServices: PluginServiceRequiredServices + ) { + this.pluginService = this.factory(params, requiredServices); } /** @@ -80,4 +119,8 @@ export class PluginServiceProvider { stop() { this.pluginService = null; } + + public get requiredServices() { + return this._requiredServices ?? []; + } } diff --git a/src/plugins/presentation_util/public/services/create/providers_mediator.ts b/src/plugins/presentation_util/public/services/create/providers_mediator.ts new file mode 100644 index 0000000000000..dd5937149850c --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/providers_mediator.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DependencyManager } from './dependency_manager'; +import { PluginServiceProviders, PluginServiceRequiredServices } from './provider'; + +export class PluginServiceProvidersMediator { + constructor(private readonly providers: PluginServiceProviders) {} + + start(params: StartParameters) { + this.getOrderedDependencies().forEach((service) => { + this.providers[service].start(params, this.getServiceDependencies(service)); + }); + } + + stop() { + this.getOrderedDependencies().forEach((service) => this.providers[service].stop()); + } + + private getOrderedDependencies() { + const dependenciesGraph = this.getGraphOfDependencies(); + return DependencyManager.orderDependencies(dependenciesGraph); + } + + private getGraphOfDependencies() { + return this.getProvidersNames().reduce>>( + (graph, vertex) => ({ ...graph, [vertex]: this.providers[vertex].requiredServices ?? [] }), + {} as Record> + ); + } + + private getProvidersNames() { + return Object.keys(this.providers) as Array; + } + + private getServiceDependencies(service: keyof Services) { + const requiredServices = this.providers[service].requiredServices ?? []; + return this.getServicesByDeps(requiredServices); + } + + private getServicesByDeps(deps: Array) { + return deps.reduce, Services>>( + (services, dependency) => ({ + ...services, + [dependency]: this.providers[dependency].getService(), + }), + {} as PluginServiceRequiredServices, Services> + ); + } +} diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx index e8f85666bcac4..8369815a042af 100644 --- a/src/plugins/presentation_util/public/services/create/registry.tsx +++ b/src/plugins/presentation_util/public/services/create/registry.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { PluginServiceProvider, PluginServiceProviders } from './provider'; +import { PluginServiceProvidersMediator } from './providers_mediator'; /** * A `PluginServiceRegistry` maintains a set of service providers which can be collectively @@ -19,10 +20,12 @@ import { PluginServiceProvider, PluginServiceProviders } from './provider'; */ export class PluginServiceRegistry { private providers: PluginServiceProviders; + private providersMediator: PluginServiceProvidersMediator; private _isStarted = false; constructor(providers: PluginServiceProviders) { this.providers = providers; + this.providersMediator = new PluginServiceProvidersMediator(providers); } /** @@ -69,8 +72,7 @@ export class PluginServiceRegistry { * @param params Parameters used to start the registry. */ start(params: StartParameters) { - const providerNames = Object.keys(this.providers) as Array; - providerNames.forEach((providerName) => this.providers[providerName].start(params)); + this.providersMediator.start(params); this._isStarted = true; return this; } @@ -79,8 +81,7 @@ export class PluginServiceRegistry { * Stop the registry. */ stop() { - const providerNames = Object.keys(this.providers) as Array; - providerNames.forEach((providerName) => this.providers[providerName].stop()); + this.providersMediator.stop(); this._isStarted = false; return this; } diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index fd8a5fd7541a6..684d29caeb312 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -86,7 +86,7 @@ export class SharePlugin implements Plugin { const { basePath } = http; this.url = new UrlService({ - baseUrl: basePath.publicBaseUrl || basePath.serverBasePath, + baseUrl: basePath.get(), version: this.initializerContext.env.packageInfo.version, navigate: async ({ app, path, state }, { replace = false } = {}) => { const [start] = await core.getStartServices(); diff --git a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx index cb2ba498a0664..063f60b82927e 100644 --- a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx @@ -18,6 +18,7 @@ import { LayoutDirection, } from '@elastic/charts'; import { EuiTitle } from '@elastic/eui'; +import { RangeFilterParams } from '@kbn/es-query'; import { useKibana } from '../../../../kibana_react/public'; import { useActiveCursor } from '../../../../charts/public'; @@ -38,7 +39,6 @@ import { getCharts } from '../helpers/plugin_services'; import type { Sheet } from '../helpers/timelion_request_handler'; import type { IInterpreterRenderHandlers } from '../../../../expressions'; import type { TimelionVisDependencies } from '../plugin'; -import type { RangeFilterParams } from '../../../../data/public'; import type { Series } from '../helpers/timelion_request_handler'; import './timelion_vis.scss'; diff --git a/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx b/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx index 136544ac068a3..edb250dfe1200 100644 --- a/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx +++ b/src/plugins/vis_types/timelion/public/legacy/timelion_vis_component.tsx @@ -11,6 +11,7 @@ import $ from 'jquery'; import moment from 'moment-timezone'; import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash'; import { useResizeObserver } from '@elastic/eui'; +import { RangeFilterParams } from '@kbn/es-query'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { useKibana } from '../../../../kibana_react/public'; @@ -31,7 +32,6 @@ import { tickFormatters } from './tick_formatters'; import { generateTicksProvider } from '../helpers/tick_generator'; import type { TimelionVisDependencies } from '../plugin'; -import type { RangeFilterParams } from '../../../../data/common'; import './timelion_vis.scss'; diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx index 3c9fb1b1b268f..6b799d3e34946 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx @@ -10,12 +10,12 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { RangeFilterParams } from '@kbn/es-query'; import { KibanaContextProvider, KibanaThemeProvider } from '../../../kibana_react/public'; import { VisualizationContainer } from '../../../visualizations/public'; import { TimelionVisDependencies } from './plugin'; import { TimelionRenderValue } from './timelion_vis_fn'; import { UI_SETTINGS } from '../common/constants'; -import { RangeFilterParams } from '../../../data/public'; const LazyTimelionVisComponent = lazy(() => import('./async_services').then(({ TimelionVisComponent }) => ({ default: TimelionVisComponent })) diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts index b84aee949471b..6ab50858fe98b 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/types.ts @@ -7,8 +7,8 @@ */ import type { IUiSettingsClient } from 'kibana/server'; +import { EsQueryConfig } from '@kbn/es-query'; import type { FetchedIndexPattern, Panel } from '../../../../../common/types'; -import type { EsQueryConfig } from '../../../../../../../data/common'; import type { SearchCapabilities } from '../../../search_strategies'; import type { VisTypeTimeseriesVisDataRequest } from '../../../../types'; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index b52a3f3a6040e..9c328a175c10b 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -12,14 +12,13 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { render } from 'react-dom'; import { EuiLoadingChart } from '@elastic/eui'; -import { Filter } from '@kbn/es-query'; +import { Filter, onlyDisabledFiltersChanged } from '@kbn/es-query'; import { KibanaThemeProvider } from '../../../kibana_react/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IndexPattern, TimeRange, Query, - esFilters, TimefilterContract, } from '../../../../plugins/data/public'; import { @@ -239,7 +238,7 @@ export class VisualizeEmbeddable } // Check if filters has changed - if (!esFilters.onlyDisabledFiltersChanged(this.input.filters, this.filters)) { + if (!onlyDisabledFiltersChanged(this.input.filters, this.filters)) { this.filters = this.input.filters; dirty = true; } diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 59d7c25c6f41d..eae4f704b7c3c 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -57,7 +57,6 @@ import { import { VisualizeLocatorDefinition } from '../common/locator'; import { showNewVisModal } from './wizard'; import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry'; -import { esFilters } from '../../../plugins/data/public'; import { FeatureCatalogueCategory } from '../../home/public'; import type { VisualizeServices } from './visualize_app/types'; @@ -189,7 +188,7 @@ export class VisualizationsPlugin ), map(({ state }) => ({ ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), + filters: data.query.filterManager.getGlobalFilters(), })) ), }, diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts index e138acf2e9e85..7fe571b25f98c 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts @@ -9,8 +9,8 @@ import { getVisualizeListItemLink } from './get_visualize_list_item_link'; import { ApplicationStart } from 'kibana/public'; import { createHashHistory } from 'history'; +import { FilterStateStore } from '@kbn/es-query'; import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; -import { esFilters } from '../../../../data/public'; import { GLOBAL_STATE_STORAGE_KEY } from '../../../common/constants'; jest.mock('../../services', () => { @@ -104,7 +104,7 @@ describe('listing item link is correct for each app', () => { }, query: { query: 'q1' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ]; diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx b/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx index 661717f99ed88..c6e8f9efdd035 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx @@ -11,6 +11,7 @@ import { cloneDeep, isEqual } from 'lodash'; import { map } from 'rxjs/operators'; import { EventEmitter } from 'events'; import { i18n } from '@kbn/i18n'; +import { FilterStateStore } from '@kbn/es-query'; import { KibanaThemeProvider, @@ -18,7 +19,7 @@ import { toMountPoint, } from '../../../../../kibana_react/public'; import { migrateLegacyQuery } from '../migrate_legacy_query'; -import { esFilters, connectToQueryState } from '../../../../../data/public'; +import { connectToQueryState } from '../../../../../data/public'; import { VisualizeServices, VisualizeAppStateContainer, @@ -87,7 +88,7 @@ export const useVisualizeAppState = ( ), }, { - filters: esFilters.FilterStateStore.APP_STATE, + filters: FilterStateStore.APP_STATE, query: true, } ); diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index 1a324ef844e2e..6636a490118b4 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -21,8 +21,7 @@ export default function ({ getService }: FtrProviderContext) { const FLIGHTS_CANVAS_APPLINK_PATH = '/app/canvas#/workpad/workpad-a474e74b-aedc-47c3-894a-db77e62c41e0'; // includes default ID of the flights canvas applink path - // Failing: See https://github.com/elastic/kibana/issues/121051 - describe.skip('sample data apis', () => { + describe('sample data apis', () => { before(async () => { await esArchiver.emptyKibanaIndex(); }); @@ -63,22 +62,23 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should load elasticsearch index containing sample data with dates relative to current time', async () => { - const resp = await es.search<{ timestamp: string }>({ - index: 'kibana_sample_data_flights', - body: { - sort: [{ timestamp: { order: 'desc' } }], - }, - }); + // Failing: See https://github.com/elastic/kibana/issues/121051 + describe.skip('dates', () => { + it('should load elasticsearch index containing sample data with dates relative to current time', async () => { + const resp = await es.search<{ timestamp: string }>({ + index: 'kibana_sample_data_flights', + body: { + sort: [{ timestamp: { order: 'desc' } }], + }, + }); - const doc = resp.hits.hits[0]; - const docMilliseconds = Date.parse(doc._source!.timestamp); - const nowMilliseconds = Date.now(); - const delta = Math.abs(nowMilliseconds - docMilliseconds); - expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 5); - }); + const doc = resp.hits.hits[0]; + const docMilliseconds = Date.parse(doc._source!.timestamp); + const nowMilliseconds = Date.now(); + const delta = Math.abs(nowMilliseconds - docMilliseconds); + expect(delta).to.be.lessThan(MILLISECOND_IN_WEEK * 5); + }); - describe('parameters', () => { it('should load elasticsearch index containing sample data with dates relative to now parameter', async () => { const nowString = `2000-01-01T00:00:00`; await supertest.post(`${apiPath}/flights?now=${nowString}`).set('kbn-xsrf', 'kibana'); diff --git a/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts index 8e7adb504ebee..439ece04e615f 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts @@ -21,7 +21,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - describe('saved objects management with hidden types', () => { + // Failing: See https://github.com/elastic/kibana/issues/116059 + describe.skip('saved objects management with hidden types', () => { before(async () => { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_types' diff --git a/x-pack/plugins/apm/public/components/app/infra_overview/index.tsx b/x-pack/plugins/apm/public/components/app/infra_overview/index.tsx new file mode 100644 index 0000000000000..c1360ba3b13ad --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/infra_overview/index.tsx @@ -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 { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export function InfraOverview() { + return ( + } + title={ +

+ {i18n.translate('xpack.apm.infra.announcement', { + defaultMessage: 'Infrastructure data coming soon', + })} +

+ } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index d8a996d2163bc..713292c633891 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -27,6 +27,7 @@ import { TransactionDetails } from '../../app/transaction_details'; import { ServiceProfiling } from '../../app/service_profiling'; import { ServiceDependencies } from '../../app/service_dependencies'; import { ServiceLogs } from '../../app/service_logs'; +import { InfraOverview } from '../../app/infra_overview'; function page({ path, @@ -265,6 +266,17 @@ export const serviceDetail = { }), element: , }), + page({ + path: '/services/{serviceName}/infra', + tab: 'infra', + title: i18n.translate('xpack.apm.views.infra.title', { + defaultMessage: 'Infrastructure', + }), + element: , + searchBarOptions: { + hidden: true, + }, + }), { path: '/services/{serviceName}/', element: , diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 0b97301b4e7c5..962fbb4eb6be6 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -42,6 +42,7 @@ type Tab = NonNullable[0] & { | 'errors' | 'metrics' | 'nodes' + | 'infra' | 'service-map' | 'logs' | 'profiling'; @@ -240,6 +241,16 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }), hidden: isJVMsTabHidden({ agentName, runtimeName }), }, + { + key: 'infra', + href: router.link('/services/{serviceName}/infra', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.infraTabLabel', { + defaultMessage: 'Infrastructure', + }), + }, { key: 'service-map', href: router.link('/services/{serviceName}/service-map', { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts index 38dfd2261da00..3a91039d81c7a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts @@ -13,9 +13,10 @@ export const areaChart: ElementFactory = () => ({ help: 'A line chart with a filled body', type: 'chart', icon: 'visArea', - expression: `filters - | demodata - | pointseries x="time" y="mean(price)" - | plot defaultStyle={seriesStyle lines=1 fill=1} - | render`, + expression: `kibana +| selectFilter +| demodata +| pointseries x="time" y="mean(price)" +| plot defaultStyle={seriesStyle lines=1 fill=1} +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts index c3f07ae601db9..b1b657bb37ff5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts @@ -15,7 +15,8 @@ export const bubbleChart: ElementFactory = () => ({ width: 700, height: 300, icon: 'heatmap', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="sum(price)" color="state" size="size(username)" | plot defaultStyle={seriesStyle points=5 fill=1} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts index d4bf6ef6f569b..c6db5ff4e3309 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts @@ -12,6 +12,7 @@ export const filterDebug: ElementFactory = () => ({ displayName: 'Debug filter', help: 'Shows the underlying global filters in a workpad', icon: 'bug', - expression: `filters + expression: `kibana +| selectFilter | render as=debug`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts index c15ca14572606..9c01259c6d9e8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts @@ -13,7 +13,8 @@ export const horizontalBarChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable horizontal bar chart', icon: 'visBarHorizontal', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="size(cost)" y="project" color="project" | plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=false diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts index f4aabba4ca216..ef278fbea3411 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts @@ -15,7 +15,8 @@ export const horizontalProgressBar: ElementFactory = () => ({ help: 'Displays progress as a portion of a horizontal bar', width: 400, height: 30, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="horizontalBar" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts index d1d723a176b45..1675c2c78cdcb 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts @@ -15,7 +15,8 @@ export const horizontalProgressPill: ElementFactory = () => ({ help: 'Displays progress as a portion of a horizontal pill', width: 400, height: 30, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="horizontalPill" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts index 84a3aee434141..cdcb9bb584b5d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts @@ -13,7 +13,8 @@ export const lineChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable line chart', icon: 'visLine', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="time" y="mean(price)" | plot defaultStyle={seriesStyle lines=3} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts index 6d8edd21c7e73..7bffff4fe95cd 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts @@ -12,7 +12,8 @@ export const markdown: ElementFactory = () => ({ type: 'text', help: 'Add text using Markdown', icon: 'visText', - expression: `filters + expression: `kibana +| selectFilter | demodata | markdown "### Welcome to the Markdown element diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index 76176f6ba2133..aa18e235f5fd9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -19,13 +19,14 @@ export const metricElementInitializer: SetupInitializer = (core, width: 200, height: 100, icon: 'visMetric', - expression: `filters - | demodata - | math "unique(country)" - | metric "Countries" - metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} - labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${core.uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" - | render`, + expression: `kibana +| selectFilter +| demodata +| math "unique(country)" +| metric "Countries" + metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} + labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${core.uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" +| render`, }); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts index 3f01a8ccb3e73..3c5a4c16565c6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts @@ -12,9 +12,10 @@ export const metricVis: ElementFactory = () => ({ type: 'chart', help: 'Metric visualization', icon: 'visMetric', - expression: `filters - | demodata - | head 1 - | metricVis metric={visdimension "percent_uptime"} colorMode="Labels" - | render`, + expression: `kibana +| selectFilter +| demodata +| head 1 +| metricVis metric={visdimension "percent_uptime"} colorMode="Labels" +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts index 2094af748ab16..4739e6ca16474 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts @@ -14,7 +14,8 @@ export const pie: ElementFactory = () => ({ height: 300, help: 'A simple pie chart', icon: 'visPie', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries color="state" size="max(price)" | pie diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts index 3e879b7fb58db..c0ebfa60708d4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts @@ -12,7 +12,8 @@ export const plot: ElementFactory = () => ({ displayName: 'Coordinate plot', type: 'chart', help: 'Mixed line, bar or dot charts', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="time" y="sum(price)" color="state" | plot defaultStyle={seriesStyle points=5} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts index e07a848263f50..85f853cea759b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts @@ -16,7 +16,8 @@ export const progressGauge: ElementFactory = () => ({ width: 200, height: 200, icon: 'visGoal', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts index 6c61ab24d13b2..100f5c65eb94a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts @@ -15,7 +15,8 @@ export const progressSemicircle: ElementFactory = () => ({ help: 'Displays progress as a portion of a semicircle', width: 200, height: 100, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="semicircle" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts index 15fec0d3b6390..1d9ffde49ff8b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts @@ -15,7 +15,8 @@ export const progressWheel: ElementFactory = () => ({ help: 'Displays progress as a portion of a wheel', width: 200, height: 200, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="wheel" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts index 783b17e7d9362..6a064ffd297ec 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts @@ -12,7 +12,8 @@ export const repeatImage: ElementFactory = () => ({ displayName: 'Image repeat', type: 'image', help: 'Repeats an image N times', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(cost)" | repeatImage image=null diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts index b2b4ea4a942a3..b78e0d1d5cf24 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts @@ -12,7 +12,8 @@ export const revealImage: ElementFactory = () => ({ displayName: 'Image reveal', type: 'image', help: 'Reveals a percentage of an image', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | revealImage origin=bottom image=null diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts index 710f595ba7179..417fe09fbc586 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts @@ -13,7 +13,8 @@ export const table: ElementFactory = () => ({ type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', icon: 'visTable', - expression: `filters + expression: `kibana +| selectFilter | demodata | table | render`, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts index b3543d532b9be..698468ab2e150 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts @@ -12,10 +12,11 @@ export const tagCloud: ElementFactory = () => ({ type: 'chart', help: 'Tagcloud visualization', icon: 'visTagCloud', - expression: `filters - | demodata - | ply by="country" fn={math "count(country)" | as "Count"} - | filterrows fn={getCell "Count" | gte 10} - | tagcloud metric={visdimension "Count"} bucket={visdimension "country"} - | render`, + expression: `kibana +| selectFilter +| demodata +| ply by="country" fn={math "count(country)" | as "Count"} +| filterrows fn={getCell "Count" | gte 10} +| tagcloud metric={visdimension "Count"} bucket={visdimension "country"} +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts index de573166c8e9a..a90f79aa995c5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts @@ -13,7 +13,8 @@ export const verticalBarChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable vertical bar chart', icon: 'visBarVertical', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="size(cost)" color="project" | plot defaultStyle={seriesStyle bars=0.75} legend=false diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts index 04ee9c8cb7db2..89ffc18766bcd 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts @@ -15,7 +15,8 @@ export const verticalProgressBar: ElementFactory = () => ({ help: 'Displays progress as a portion of a vertical bar', width: 80, height: 400, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="verticalBar" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts index 7bbf3874f175f..b3a977c1d795a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts @@ -15,7 +15,8 @@ export const verticalProgressPill: ElementFactory = () => ({ help: 'Displays progress as a portion of a vertical pill', width: 80, height: 400, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="verticalPill" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts index c0ed25849ac97..66553b6fda6c0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts @@ -47,13 +47,14 @@ export function exactly(): ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const { value, column } = args; + const { value, column, filterGroup } = args; const filter: ExpressionValueFilter = { type: 'filter', filterType: 'exactly', value, column, + filterGroup, and: [], }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts index e4a6a102844a9..b61e03319b916 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts @@ -58,11 +58,12 @@ export function timefilter(): ExpressionFunctionDefinition< return input; } - const { from, to, column } = args; + const { from, to, column, filterGroup } = args; const filter: ExpressionValueFilter = { type: 'filter', filterType: 'time', column, + filterGroup, and: [], }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts new file mode 100644 index 0000000000000..75bd97421e58e --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.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 { fromExpression } from '@kbn/interpreter'; +import { filters } from './filters'; + +const { migrations } = filters(); + +describe('filters migrations', () => { + const expression = 'filters group="1" group="3" ungrouped=true'; + const ast = fromExpression(expression); + it('8.1.0. Should migrate `filters` expression to `kibana | selectFilter`', () => { + const migratedAst = migrations?.['8.1.0'](ast.chain[0]); + expect(migratedAst !== null && typeof migratedAst === 'object').toBeTruthy(); + expect(migratedAst.type).toBe('expression'); + expect(Array.isArray(migratedAst.chain)).toBeTruthy(); + expect(migratedAst.chain[0].function === 'kibana').toBeTruthy(); + expect(migratedAst.chain[0].arguments).toEqual({}); + expect(migratedAst.chain[1].function === 'selectFilter').toBeTruthy(); + expect(migratedAst.chain[1].arguments).toEqual(ast.chain[0].arguments); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts new file mode 100644 index 0000000000000..8b46e818209f3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.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 { + ExpressionValueFilter, + ExpressionAstExpression, + ExpressionAstFunction, +} from 'src/plugins/expressions'; +import { fromExpression } from '@kbn/interpreter'; +import { buildFiltersFunction } from '../../../common/functions'; +import type { FiltersFunction } from '../../../common/functions'; + +/* + Expression function `filters` can't be used on the server, because it is tightly coupled with the redux store. + It is replaced with `kibana | selectFilter`. + + Current filters function definition is used only for the purpose of enabling migrations. + The function has to be registered on the server while the plugin's setup, to be able to run its migration. +*/ +const filtersFn = (): ExpressionValueFilter => ({ + type: 'filter', + and: [], +}); + +const migrations: FiltersFunction['migrations'] = { + '8.1.0': (ast: ExpressionAstFunction): ExpressionAstFunction | ExpressionAstExpression => { + const SELECT_FILTERS = 'selectFilter'; + const newExpression = `kibana | ${SELECT_FILTERS}`; + const newAst: ExpressionAstExpression = fromExpression(newExpression); + const selectFiltersAstIndex = newAst.chain.findIndex( + ({ function: fnName }) => fnName === SELECT_FILTERS + ); + const selectFilterAst = newAst.chain[selectFiltersAstIndex]; + newAst.chain.splice(selectFiltersAstIndex, 1, { ...selectFilterAst, arguments: ast.arguments }); + return newAst; + }, +}; + +export const filters = buildFiltersFunction(filtersFn, migrations); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts index ae3778366651c..388db9e6e5960 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts @@ -7,5 +7,6 @@ import { demodata } from './demodata'; import { pointseries } from './pointseries'; +import { filters } from './filters'; -export const functions = [demodata, pointseries]; +export const functions = [filters, demodata, pointseries]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx index 643d7cdedc50d..38d1d502704e2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -21,7 +21,6 @@ export const defaultHandlers: RendererHandlers = { onEmbeddableInputChange: action('onEmbeddableInputChange'), onResize: action('onResize'), resize: action('resize'), - setFilter: action('setFilter'), done: action('done'), onDestroy: action('onDestroy'), reload: action('reload'), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx index b831c9aa70e49..a31021cba4c10 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx @@ -25,7 +25,10 @@ export const advancedFilterFactory: StartInitializer> = render(domNode, _, handlers) { ReactDOM.render( - + handlers.event({ name: 'applyFilterAction', data: filter })} + value={handlers.getFilter()} + /> , domNode, () => handlers.done() diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index 372bcbb5642cb..5e4ea42990e47 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -55,20 +55,19 @@ export const dropdownFilterFactory: StartInitializer> = (filterExpression === undefined || !filterExpression.includes('exactly')) ) { filterExpression = ''; - handlers.setFilter(filterExpression); + handlers.event({ name: 'applyFilterAction', data: filterExpression }); } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed const { changed, newAst } = syncFilterExpression(config, filterExpression, ['filterGroup']); if (changed) { - handlers.setFilter(toExpression(newAst)); + handlers.event({ name: 'applyFilterAction', data: toExpression(newAst) }); } } - const commit = (commitValue: string) => { if (commitValue === '%%CANVAS_MATCH_ALL%%') { - handlers.setFilter(''); + handlers.event({ name: 'applyFilterAction', data: '' }); } else { const newFilterAST: Ast = { type: 'expression', @@ -86,18 +85,19 @@ export const dropdownFilterFactory: StartInitializer> = }; const newFilter = toExpression(newFilterAST); - handlers.setFilter(newFilter); + handlers.event({ name: 'applyFilterAction', data: newFilter }); } }; + const filter = ( + + ); ReactDOM.render( - - - , + {filter}, domNode, () => handlers.done() ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index e81ca2cc1f057..f7e9d333f8683 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -45,7 +45,7 @@ export const timeFilterFactory: StartInitializer> = ( if (filterExpression === undefined || filterExpression.indexOf('timefilter') !== 0) { filterExpression = defaultTimeFilterExpression; - handlers.setFilter(filterExpression); + handlers.event({ name: 'applyFilterAction', data: filterExpression }); } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed @@ -55,14 +55,14 @@ export const timeFilterFactory: StartInitializer> = ( ]); if (changed) { - handlers.setFilter(toExpression(newAst)); + handlers.event({ name: 'applyFilterAction', data: toExpression(newAst) }); } } ReactDOM.render( handlers.event({ name: 'applyFilterAction', data: filter })} filter={filterExpression} commonlyUsedRanges={customQuickRanges} dateFormat={customDateFormat} diff --git a/x-pack/plugins/canvas/common/functions/filters.ts b/x-pack/plugins/canvas/common/functions/filters.ts new file mode 100644 index 0000000000000..5c48fbd10862a --- /dev/null +++ b/x-pack/plugins/canvas/common/functions/filters.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; +import { ExpressionValueFilter } from '../../types'; +import { getFunctionHelp } from '../../i18n'; + +export interface Arguments { + group: string[]; + ungrouped: boolean; +} + +export type FiltersFunction = ExpressionFunctionDefinition< + 'filters', + null, + Arguments, + ExpressionValueFilter +>; + +export function buildFiltersFunction( + fn: FiltersFunction['fn'], + migrations?: FiltersFunction['migrations'] +) { + return function filters(): FiltersFunction { + const { help, args: argHelp } = getFunctionHelp().filters; + + return { + name: 'filters', + type: 'filter', + help, + context: { + types: ['null'], + }, + args: { + group: { + aliases: ['_'], + types: ['string'], + help: argHelp.group, + multi: true, + }, + ungrouped: { + aliases: ['nogroup', 'nogroups'], + types: ['boolean'], + help: argHelp.ungrouped, + default: false, + }, + }, + fn, + migrations, + }; + }; +} diff --git a/x-pack/plugins/canvas/common/functions/index.ts b/x-pack/plugins/canvas/common/functions/index.ts new file mode 100644 index 0000000000000..08d9391f81c13 --- /dev/null +++ b/x-pack/plugins/canvas/common/functions/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 type { FiltersFunction } from './filters'; +export { buildFiltersFunction } from './filters'; diff --git a/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts index 57fdc7d7309ce..c98d2f080452a 100644 --- a/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts +++ b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts @@ -6,6 +6,8 @@ */ import { buildQueryFilter, Filter } from '@kbn/es-query'; +import dateMath from '@elastic/datemath'; +import { maxBy, minBy } from 'lodash'; import { ExpressionValueFilter } from '../../types'; // @ts-expect-error untyped local import { buildBoolArray } from './build_bool_array'; @@ -16,24 +18,45 @@ export interface EmbeddableFilterInput { timeRange?: TimeRange; } +type ESFilter = Record; + const TimeFilterType = 'time'; +const formatTime = (str: string | undefined, roundUp: boolean = false) => { + if (!str) { + return null; + } + const moment = dateMath.parse(str, { roundUp }); + return !moment || !moment.isValid() ? null : moment.valueOf(); +}; + function getTimeRangeFromFilters(filters: ExpressionValueFilter[]): TimeRange | undefined { - const timeFilter = filters.find( - (filter) => filter.filterType !== undefined && filter.filterType === TimeFilterType + const timeFilters = filters.filter( + (filter) => + filter.filterType !== undefined && + filter.filterType === TimeFilterType && + filter.from !== undefined && + filter.to !== undefined ); - return timeFilter !== undefined && timeFilter.from !== undefined && timeFilter.to !== undefined - ? { - from: timeFilter.from, - to: timeFilter.to, - } + const validatedTimeFilters = timeFilters.filter( + (filter) => formatTime(filter.from) !== null && formatTime(filter.to, true) !== null + ); + + const minFromFilter = minBy(validatedTimeFilters, (filter) => formatTime(filter.from)); + const maxToFilter = maxBy(validatedTimeFilters, (filter) => formatTime(filter.to, true)); + + return minFromFilter?.from && maxToFilter?.to + ? { from: minFromFilter.from, to: maxToFilter.to } : undefined; } export function getQueryFilters(filters: ExpressionValueFilter[]): Filter[] { const dataFilters = filters.map((filter) => ({ ...filter, type: filter.filterType })); - return buildBoolArray(dataFilters).map(buildQueryFilter); + return buildBoolArray(dataFilters).map((filter: ESFilter, index: number) => { + const { group, ...restFilter } = filter; + return buildQueryFilter(restFilter, index.toString(), '', { group }); + }); } export function buildEmbeddableFilters(filters: ExpressionValueFilter[]): EmbeddableFilterInput { diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts index 6a61ec595acb7..fa938f2c07c74 100644 --- a/x-pack/plugins/canvas/common/lib/constants.ts +++ b/x-pack/plugins/canvas/common/lib/constants.ts @@ -16,6 +16,8 @@ export const APP_ROUTE = '/app/canvas'; export const APP_ROUTE_WORKPAD = `${APP_ROUTE}#/workpad`; export const API_ROUTE = '/api/canvas'; export const API_ROUTE_WORKPAD = `${API_ROUTE}/workpad`; +export const API_ROUTE_WORKPAD_EXPORT = `${API_ROUTE_WORKPAD}/export`; +export const API_ROUTE_WORKPAD_IMPORT = `${API_ROUTE_WORKPAD}/import`; export const API_ROUTE_WORKPAD_ASSETS = `${API_ROUTE}/workpad-assets`; export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`; export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`; diff --git a/x-pack/plugins/canvas/common/lib/filters.js b/x-pack/plugins/canvas/common/lib/filters.js index 08caded52aa26..f43e2dd3b4606 100644 --- a/x-pack/plugins/canvas/common/lib/filters.js +++ b/x-pack/plugins/canvas/common/lib/filters.js @@ -13,15 +13,16 @@ export function time(filter) { if (!filter.column) { throw new Error('column is required for Elasticsearch range filters'); } + const { from, to, column, filterGroup: group } = filter; return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, + group, + range: { [column]: { gte: from, lte: to } }, }; } export function luceneQueryString(filter) { return { + group: filter.filterGroup, query_string: { query: filter.query || '*', }, @@ -30,6 +31,7 @@ export function luceneQueryString(filter) { export function exactly(filter) { return { + group: filter.filterGroup, term: { [filter.column]: { value: filter.value, 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 89faef29a3b02..1ca674bfb6f9d 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 @@ -7,16 +7,19 @@ import React, { useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; -import { interpretAst } from '../../../lib/run_interpreter'; import { Loading } from '../../loading'; +import { useExpressionsService } from '../../../services'; import { DatasourcePreview as Component } from './datasource_preview'; export const DatasourcePreview = (props) => { const [datatable, setDatatable] = useState(); + const expressionsService = useExpressionsService(); useEffect(() => { - interpretAst({ type: 'expression', chain: [props.function] }, {}).then(setDatatable); - }, [props.function, setDatatable]); + expressionsService + .interpretAst({ type: 'expression', chain: [props.function] }, {}) + .then(setDatatable); + }, [expressionsService, props.function, setDatatable]); if (!datatable) { return ; 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 6048ac360386c..31db3366ce3b5 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 'recompose'; import { get } from 'lodash'; import { toExpression } from '@kbn/interpreter'; -import { interpretAst } from '../../lib/run_interpreter'; +import { pluginServices } from '../../services'; import { getArgTypeDef } from '../../lib/args'; import { FunctionFormList as Component } from './function_form_list'; @@ -77,24 +77,27 @@ const componentFactory = ({ path, parentPath, removable, -}) => ({ - args, - nestedFunctionsArgs: argsWithExprFunctions, - argType: argType.function, - argTypeDef: Object.assign(argTypeDef, { - args: argumentsView, - name: argUiConfig?.name ?? argTypeDef.name, - displayName: argUiConfig?.displayName ?? argTypeDef.displayName, - help: argUiConfig?.help ?? argTypeDef.name, - }), - argResolver: (argAst) => interpretAst(argAst, prevContext), - contextExpression: getExpression(prevContext), - expressionIndex, // preserve the index in the AST - nextArgType: nextArg && nextArg.function, - path, - parentPath, - removable, -}); +}) => { + const { expressions } = pluginServices.getServices(); + return { + args, + nestedFunctionsArgs: argsWithExprFunctions, + argType: argType.function, + argTypeDef: Object.assign(argTypeDef, { + args: argumentsView, + name: argUiConfig?.name ?? argTypeDef.name, + displayName: argUiConfig?.displayName ?? argTypeDef.displayName, + help: argUiConfig?.help ?? argTypeDef.name, + }), + argResolver: (argAst) => expressions.interpretAst(argAst, prevContext), + contextExpression: getExpression(prevContext), + expressionIndex, // preserve the index in the AST + nextArgType: nextArg && nextArg.function, + path, + parentPath, + removable, + }; +}; /** * Converts expression functions at the arguments for the expression, to the array of UI component configurations. diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts index a8409270752ad..785f183b193f1 100644 --- a/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts @@ -22,7 +22,8 @@ export const getFunctionExamples = (): FunctionExampleDict => ({ syntax: `all {neq "foo"} {neq "bar"} {neq "fizz"} all condition={gt 10} condition={lt 20}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | formatnumber "0.0%" @@ -42,7 +43,8 @@ all condition={gt 10} condition={lt 20}`, syntax: `alterColumn "cost" type="string" alterColumn column="@timestamp" name="foo"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | alterColumn "time" name="time_in_ms" type="number" | table @@ -54,7 +56,8 @@ alterColumn column="@timestamp" name="foo"`, syntax: `any {eq "foo"} {eq "bar"} {eq "fizz"} any condition={lte 10} condition={gt 30}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | filterrows { getCell "project" | any {eq "elasticsearch"} {eq "kibana"} {eq "x-pack"} @@ -70,7 +73,8 @@ any condition={lte 10} condition={gt 30}`, as "foo" as name="bar"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | ply by="project" fn={math "count(username)" | as "num_users"} fn={math "mean(price)" | as "price"} | pointseries x="project" y="num_users" size="price" color="project" @@ -94,7 +98,8 @@ asset id="asset-498f7429-4d56-42a2-a7e4-8bf08d98d114"`, syntax: `axisConfig show=false axisConfig position="right" min=0 max=10 tickSize=1`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="size(cost)" y="project" color="project" | plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} @@ -133,7 +138,8 @@ case if={lte 50} then="green"`, syntax: `columns include="@timestamp, projects, cost" columns exclude="username, country, age"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | columns include="price, cost, state, project" | table @@ -145,7 +151,8 @@ columns exclude="username, country, age"`, syntax: `compare "neq" to="elasticsearch" compare op="lte" to=100`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn project fn={getCell project | @@ -229,7 +236,8 @@ date "01/31/2019" format="MM/DD/YYYY"`, demodata "ci" demodata type="shirts"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | table | render`, @@ -252,7 +260,8 @@ eq null eq 10 eq "foo"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn project fn={getCell project | @@ -272,7 +281,8 @@ eq "foo"`, escount "currency:\"EUR\"" index="kibana_sample_data_ecommerce" escount query="response:404" index="kibana_sample_data_logs"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | escount "Cancelled:true" index="kibana_sample_data_flights" | math "value" | progress shape="semicircle" @@ -290,7 +300,8 @@ esdocs query="response:404" index="kibana_sample_data_logs" esdocs index="kibana_sample_data_flights" count=100 esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | esdocs index="kibana_sample_data_ecommerce" fields="customer_gender, taxful_total_price, order_date" sort="order_date, asc" @@ -309,7 +320,8 @@ esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc"`, syntax: `essql query="SELECT * FROM \"logstash*\"" essql "SELECT * FROM \"apm*\"" count=10000`, usage: { - expression: `filters + expression: `kibana +| selectFilter | essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM \"kibana_sample_data_flights\"" | table | render`, @@ -321,7 +333,8 @@ essql "SELECT * FROM \"apm*\"" count=10000`, exactly "age" value=50 filterGroup="group2" exactly column="project" value="beats"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | exactly column=project value=elasticsearch | demodata | pointseries x=project y="mean(age)" @@ -334,7 +347,8 @@ exactly column="project" value="beats"`, syntax: `filterrows {getCell "project" | eq "kibana"} filterrows fn={getCell "age" | gt 50}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | filterrows {getCell "country" | any {eq "IN"} {eq "US"} {eq "CN"}} | mapColumn "@timestamp" @@ -379,7 +393,8 @@ font underline=true font italic=false font lHeight=32`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="size(cost)" color="project" | plot defaultStyle={seriesStyle bars=0.75} legend=false @@ -399,7 +414,8 @@ font lHeight=32`, syntax: `formatdate format="YYYY-MM-DD" formatdate "MM/DD/YYYY"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn "time" fn={getCell time | formatdate "MMM 'YY"} | pointseries x="time" y="sum(price)" color="state" @@ -412,7 +428,8 @@ formatdate "MM/DD/YYYY"`, syntax: `formatnumber format="$0,0.00" formatnumber "0.0a"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" 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 eb87f4720deec..3290bc8227a29 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 @@ -29,7 +29,7 @@ export const useCreateWorkpad = () => { history.push(`/workpad/${workpad.id}/page/1`); } catch (err) { notifyService.error(err, { - title: errors.getUploadFailureErrorMessage(), + title: errors.getCreateFailureErrorMessage(), }); } return; @@ -39,8 +39,8 @@ export const useCreateWorkpad = () => { }; const errors = { - getUploadFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', { - defaultMessage: `Couldn't upload workpad`, + getCreateFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useCreateWorkpad.createFailureErrorMessage', { + defaultMessage: `Couldn't create workpad`, }), }; 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 new file mode 100644 index 0000000000000..8c8d2e26d8a22 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.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 { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +// @ts-expect-error +import { getDefaultWorkpad } from '../../../state/defaults'; +import { useNotifyService, useWorkpadService } from '../../../services'; + +import type { CanvasWorkpad } from '../../../../types'; + +export const useImportWorkpad = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (workpad: CanvasWorkpad) => { + try { + const importedWorkpad = await workpadService.import(workpad); + history.push(`/workpad/${importedWorkpad.id}/page/1`); + } catch (err) { + notifyService.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); + } + return; + }, + [notifyService, history, workpadService] + ); +}; + +const errors = { + getUploadFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage', { + defaultMessage: `Couldn't upload workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts index caec30e083d40..045ff8b52e259 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts @@ -9,19 +9,25 @@ import { useCallback } from 'react'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { SavedObject } from 'kibana/public'; import { CANVAS, JSON as JSONString } from '../../../../i18n/constants'; import { useNotifyService } from '../../../services'; import { getId } from '../../../lib/get_id'; - -import { useCreateWorkpad } from './use_create_workpad'; +import { useImportWorkpad as useImportWorkpadHook } from './use_import_workpad'; import type { CanvasWorkpad } from '../../../../types'; +const isInvalidWorkpad = (workpad: CanvasWorkpad) => + !Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets; + export const useImportWorkpad = () => { const notifyService = useNotifyService(); - const createWorkpad = useCreateWorkpad(); + const importWorkpad = useImportWorkpadHook(); return useCallback( - (file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => { + ( + file?: File, + onComplete: (workpad?: CanvasWorkpad | SavedObject) => void = () => {} + ) => { if (!file) { onComplete(); return; @@ -42,16 +48,17 @@ export const useImportWorkpad = () => { // handle reading the uploaded file reader.onload = async () => { try { - const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + const workpad: CanvasWorkpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + workpad.id = getId('workpad'); // sanity check for workpad object - if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) { + if (isInvalidWorkpad(workpad)) { onComplete(); throw new Error(errors.getMissingPropertiesErrorMessage()); } - await createWorkpad(workpad); + await importWorkpad(workpad); onComplete(workpad); } catch (e) { notifyService.error(e, { @@ -66,7 +73,7 @@ export const useImportWorkpad = () => { // read the uploaded file reader.readAsText(file); }, - [notifyService, createWorkpad] + [notifyService, importWorkpad] ); }; 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 85b195214d44b..21bcc89304b3c 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,19 +5,22 @@ * 2.0. */ -import { fromExpression } from '@kbn/interpreter'; +import { ExpressionFunctionAST, fromExpression } from '@kbn/interpreter'; import { shallowEqual, useSelector } from 'react-redux'; import { State } from '../../../../types'; -import { getFiltersByGroups } from '../../../lib/filter'; +import { getFiltersByFilterExpressions } from '../../../lib/filter'; import { adaptCanvasFilter } from '../../../lib/filter_adapters'; -import { getGlobalFilters } from '../../../state/selectors/workpad'; +import { useFiltersService } from '../../../services'; -const extractExpressionAST = (filtersExpressions: string[]) => - fromExpression(filtersExpressions.join(' | ')); +const extractExpressionAST = (filters: string[]) => fromExpression(filters.join(' | ')); -export function useCanvasFilters(groups: string[] = [], ungrouped: boolean = false) { - const filterExpressions = useSelector((state: State) => getGlobalFilters(state), shallowEqual); - const filtersByGroups = getFiltersByGroups(filterExpressions, groups, ungrouped); +export function useCanvasFilters(filterExprsToGroupBy: ExpressionFunctionAST[] = []) { + const filtersService = useFiltersService(); + const filterExpressions = useSelector( + (state: State) => filtersService.getFilters(state), + shallowEqual + ); + const filtersByGroups = getFiltersByFilterExpressions(filterExpressions, filterExprsToGroupBy); const expression = extractExpressionAST(filtersByGroups); const filters = expression.chain.map(adaptCanvasFilter); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx index 610e6e56af350..20ec56706480d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx @@ -8,11 +8,7 @@ import React, { FC, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { State, FilterField, PositionedElement } from '../../../types'; -import { - extractGroupsFromElementsFilters, - groupFiltersBy, - extractUngroupedFromElementsFilters, -} from '../../lib/filter'; +import { groupFiltersBy, getFiltersExprsFromExpression } from '../../lib/filter'; import { setGroupFiltersByOption } from '../../state/actions/sidebar'; import { getGroupFiltersByOption } from '../../state/selectors/sidebar'; import { useCanvasFilters } from './hooks'; @@ -35,11 +31,8 @@ export const WorkpadFilters: FC = ({ element }) => { }, [dispatch] ); - - const groups = element ? extractGroupsFromElementsFilters(element.expression) : undefined; - const ungrouped = element ? extractUngroupedFromElementsFilters(element.expression) : false; - - const canvasFilters = useCanvasFilters(groups, ungrouped); + const filterExprs = element ? getFiltersExprsFromExpression(element.expression) : []; + const canvasFilters = useCanvasFilters(filterExprs); const filtersGroups = groupFiltersByField ? groupFiltersBy(canvasFilters, groupFiltersByField) diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx index 9d37873bcae0a..62d070dbf00f5 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx @@ -17,7 +17,8 @@ const testElements: { [key: string]: ElementSpec } = { displayName: 'Area chart', help: 'A line chart with a filled body', type: 'chart', - expression: `filters + expression: `kibana + | selectFilter | demodata | pointseries x="time" y="mean(price)" | plot defaultStyle={seriesStyle lines=1 fill=1} @@ -47,7 +48,8 @@ const testElements: { [key: string]: ElementSpec } = { displayName: 'Debug filter', help: 'Shows the underlying global filters in a workpad', icon: 'bug', - expression: `filters + expression: `kibana + | selectFilter | render as=debug`, }, image: { @@ -64,7 +66,8 @@ const testElements: { [key: string]: ElementSpec } = { type: 'text', help: 'Add text using Markdown', icon: 'visText', - expression: `filters + expression: `kibana +| selectFilter | demodata | markdown "### Welcome to the Markdown element @@ -89,7 +92,8 @@ You can use standard Markdown in here, but you can also access your piped-in dat width: 200, height: 200, icon: 'visGoal', - expression: `filters + expression: `kibana + | selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="Helvetica" color="#000000" align=center} @@ -111,7 +115,8 @@ You can use standard Markdown in here, but you can also access your piped-in dat displayName: 'Data table', type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', - expression: `filters + expression: `kibana + | selectFilter | demodata | table | render`, diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index 2634d76297b58..a168020b6eef8 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -7,15 +7,10 @@ import { fromExpression } from '@kbn/interpreter'; import { get } from 'lodash'; -import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; -import { interpretAst } from '../lib/run_interpreter'; -// @ts-expect-error untyped local -import { getState } from '../state/store'; -import { getGlobalFilters, getWorkpadVariablesAsObject } from '../state/selectors/workpad'; -import { ExpressionValueFilter } from '../../types'; -import { getFunctionHelp } from '../../i18n'; +import { pluginServices } from '../services'; +import type { FiltersFunction } from '../../common/functions'; +import { buildFiltersFunction } from '../../common/functions'; import { InitializeArguments } from '.'; -import { getFiltersByGroups } from '../lib/filter'; export interface Arguments { group: string[]; @@ -31,58 +26,34 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = // remove all allFilters that belong to a group return allFilters.filter((filter: string) => { const ast = fromExpression(filter); - const expGroups = get(ast, 'chain[0].arguments.filterGroup', []); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); return expGroups.length === 0; }); } - return getFiltersByGroups(allFilters, groups); + return allFilters.filter((filter: string) => { + const ast = fromExpression(filter); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); + return expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup)); + }); } -type FiltersFunction = ExpressionFunctionDefinition< - 'filters', - null, - Arguments, - ExpressionValueFilter ->; - export function filtersFunctionFactory(initialize: InitializeArguments): () => FiltersFunction { - return function filters(): FiltersFunction { - const { help, args: argHelp } = getFunctionHelp().filters; - - return { - name: 'filters', - type: 'filter', - help, - context: { - types: ['null'], - }, - args: { - group: { - aliases: ['_'], - types: ['string'], - help: argHelp.group, - multi: true, - }, - ungrouped: { - aliases: ['nogroup', 'nogroups'], - types: ['boolean'], - help: argHelp.ungrouped, - default: false, - }, - }, - fn: (input, { group, ungrouped }) => { - const filterList = getFiltersByGroup(getGlobalFilters(getState()), group, ungrouped); - - if (filterList && filterList.length) { - const filterExpression = filterList.join(' | '); - const filterAST = fromExpression(filterExpression); - return interpretAst(filterAST, getWorkpadVariablesAsObject(getState())); - } else { - const filterType = initialize.types.filter; - return filterType?.from(null, {}); - } - }, - }; + const fn: FiltersFunction['fn'] = (input, { group, ungrouped }) => { + const { expressions, filters: filtersService } = pluginServices.getServices(); + + const filterList = getFiltersByGroup(filtersService.getFilters(), group, ungrouped); + + if (filterList && filterList.length) { + const filterExpression = filterList.join(' | '); + const filterAST = fromExpression(filterExpression); + const { variables } = filtersService.getFiltersContext(); + return expressions.interpretAst(filterAST, variables); + } else { + const filterType = initialize.types.filter; + return filterType?.from(null, {}); + } }; + + return buildFiltersFunction(fn); } diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 3734b1bf53051..3536bed0f92b3 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -10,10 +10,9 @@ import { ExpressionRendererEvent, IInterpreterRenderHandlers, } from 'src/plugins/expressions/public'; -// @ts-expect-error untyped local -import { setFilter } from '../state/actions/elements'; import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable'; import { RendererHandlers, CanvasElement } from '../../types'; +import { pluginServices } from '../services'; import { clearValue } from '../state/actions/resolved_args'; // This class creates stub handlers to ensure every element and renderer fulfills the contract. @@ -58,7 +57,6 @@ export const createHandlers = (baseHandlers = createBaseHandlers()): RendererHan }, resize(_size: { height: number; width: number }) {}, - setFilter() {}, }); export const assignHandlers = (handlers: Partial = {}): RendererHandlers => @@ -79,6 +77,8 @@ export const createDispatchedHandlerFactory = ( oldElement = element; } + const { filters } = pluginServices.getServices(); + const handlers: RendererHandlers & { event: IInterpreterRenderHandlers['event']; done: IInterpreterRenderHandlers['done']; @@ -89,8 +89,8 @@ export const createDispatchedHandlerFactory = ( case 'embeddableInputChange': this.onEmbeddableInputChange(event.data); break; - case 'setFilter': - this.setFilter(event.data); + case 'applyFilterAction': + filters.updateFilter(element.id, event.data); break; case 'onComplete': this.onComplete(event.data); @@ -106,10 +106,6 @@ export const createDispatchedHandlerFactory = ( break; } }, - setFilter(text: string) { - dispatch(setFilter(text, element.id, true)); - }, - getFilter() { return element.filter || ''; }, diff --git a/x-pack/plugins/canvas/public/lib/filter.test.ts b/x-pack/plugins/canvas/public/lib/filter.test.ts index bf19bd6ecf4b8..9aef71f33f609 100644 --- a/x-pack/plugins/canvas/public/lib/filter.test.ts +++ b/x-pack/plugins/canvas/public/lib/filter.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { fromExpression } from '@kbn/interpreter'; import { FC } from 'react'; import { Filter as FilterType, @@ -18,9 +19,8 @@ import { flattenFilterView, createFilledFilterView, groupFiltersBy, - getFiltersByGroups, - extractGroupsFromElementsFilters, - extractUngroupedFromElementsFilters, + getFiltersExprsFromExpression, + getFiltersByFilterExpressions, isExpressionWithFilters, } from './filter'; @@ -285,7 +285,7 @@ describe('groupFiltersBy', () => { }); }); -describe('getFiltersByGroups', () => { +describe('getFiltersByFilterExpressions', () => { const group1 = 'Group 1'; const group2 = 'Group 2'; @@ -296,66 +296,106 @@ describe('getFiltersByGroups', () => { `exactly value="kibana" column="project2" filterGroup="${group2}"`, ]; - it('returns all filters related to a specified groups', () => { - expect(getFiltersByGroups(filters, [group1, group2])).toEqual([ - filters[0], - filters[1], - filters[3], - ]); + const filtersExprWithGroup = `filters group="${group2}"`; + + const kibanaExpr = 'kibana'; + const selectFilterExprEmpty = 'selectFilter'; + const selectFilterExprWithGroup = `${selectFilterExprEmpty} group="${group2}"`; + const selectFilterExprWithGroups = `${selectFilterExprEmpty} group="${group2}" group="${group1}"`; + const selectFilterExprWithUngrouped = `${selectFilterExprEmpty} ungrouped=true`; + const selectFilterExprWithGroupAndUngrouped = `${selectFilterExprEmpty} group="${group2}" ungrouped=true`; + + const removeFilterExprEmpty = 'removeFilter'; + const removeFilterExprWithGroup = `${removeFilterExprEmpty} group="${group2}"`; + const removeFilterExprWithUngrouped = `${removeFilterExprEmpty} ungrouped=true`; + const removeFilterExprWithGroupAndUngrouped = `${removeFilterExprEmpty} group="${group2}" ungrouped=true`; + + const getFiltersAsts = (filtersExprs: string[]) => { + const ast = fromExpression(filtersExprs.join(' | ')); + return ast.chain; + }; - expect(getFiltersByGroups(filters, [group2])).toEqual([filters[1], filters[3]]); + it('returns all filters if no arguments specified to selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprEmpty]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual(filters); }); - it('returns filters without group if ungrouped is true', () => { - expect(getFiltersByGroups(filters, [], true)).toEqual([filters[2]]); + it('returns filters with group, specified to selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithGroups]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[1], filters[3]]); }); - it('returns filters with group if ungrouped is true and groups are not empty', () => { - expect(getFiltersByGroups(filters, [group1], true)).toEqual([filters[0]]); + it('returns filters without group if ungrouped is true at selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[2]]); }); - it('returns empty array if not found any filter with a specified group', () => { - expect(getFiltersByGroups(filters, ['absent group'])).toEqual([]); + it('returns filters with group if ungrouped is true and groups are not empty at selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithGroupAndUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[1], filters[2], filters[3]]); }); - it('returns empty array if not groups specified', () => { - expect(getFiltersByGroups(filters, [])).toEqual(filters); + it('returns no filters if no arguments, specified to removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprEmpty]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([]); + }); + + it('returns filters without group, specified to removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithGroup]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[2]]); }); -}); -describe('extractGroupsFromElementsFilters', () => { - const exprFilters = 'filters'; - const exprRest = 'demodata | plot | render'; - - it('returns groups which are specified at filters expression', () => { - const groups = ['group 1', 'group 2', 'group 3', 'group 4']; - const additionalGroups = [...groups, 'group 5']; - const groupsExpr = groups.map((group) => `group="${group}"`).join(' '); - const additionalGroupsExpr = additionalGroups.map((group) => `group="${group}"`).join(' '); - - expect( - extractGroupsFromElementsFilters( - `${exprFilters} ${groupsExpr} | ${exprFilters} ${additionalGroupsExpr} | ${exprRest}` - ) - ).toEqual(additionalGroups); + it('returns filters without group if ungrouped is true at removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[1], filters[3]]); }); - it('returns empty array if no groups were specified at filters expression', () => { - expect(extractGroupsFromElementsFilters(`${exprFilters} | ${exprRest}`)).toEqual([]); + it('remove filters without group and with specified group if ungrouped is true and groups are not empty at removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithGroupAndUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0]]); + }); + + it('should include/exclude filters iteratively', () => { + const filtersExprs = getFiltersAsts([ + kibanaExpr, + selectFilterExprWithGroup, + removeFilterExprWithGroup, + selectFilterExprEmpty, + ]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([]); + }); + + it('should include/exclude filters from global filters if `filters` expression is specified', () => { + const filtersExprs = getFiltersAsts([ + kibanaExpr, + selectFilterExprWithGroup, + removeFilterExprWithGroup, + selectFilterExprEmpty, + filtersExprWithGroup, + ]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[1], filters[3]]); }); }); -describe('extractUngroupedFromElementsFilters', () => { - it('checks if ungrouped filters expression exist at the element', () => { - const expression = - 'filters group="10" group="11" | filters group="15" ungrouped=true | demodata | plot | render'; - const isUngrouped = extractUngroupedFromElementsFilters(expression); - expect(isUngrouped).toBeTruthy(); +describe('getFiltersExprsFromExpression', () => { + it('returns list of filters expressions asts', () => { + const filter1 = 'selectFilter'; + const filter2 = 'filters group="15" ungrouped=true'; + const filter3 = 'removeFilter'; + const expression = `kibana | ${filter1} | ${filter2} | ${filter3} | demodata | plot | render`; + const filtersAsts = getFiltersExprsFromExpression(expression); - const nextExpression = - 'filters group="10" group="11" | filters group="15" | demodata | plot | render'; - const nextIsUngrouped = extractUngroupedFromElementsFilters(nextExpression); - expect(nextIsUngrouped).toBeFalsy(); + expect(filtersAsts).toEqual([filter1, filter2, filter3].map((f) => fromExpression(f).chain[0])); }); }); diff --git a/x-pack/plugins/canvas/public/lib/filter.ts b/x-pack/plugins/canvas/public/lib/filter.ts index 6e9db1757ccc7..2554ae11220eb 100644 --- a/x-pack/plugins/canvas/public/lib/filter.ts +++ b/x-pack/plugins/canvas/public/lib/filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { fromExpression } from '@kbn/interpreter'; +import { Ast, ExpressionFunctionAST, fromExpression, toExpression } from '@kbn/interpreter'; import { flowRight, get, groupBy } from 'lodash'; import { Filter as FilterType, @@ -14,6 +14,14 @@ import { FlattenFilterViewInstance, } from '../../types/filters'; +const SELECT_FILTER = 'selectFilter'; +const FILTERS = 'filters'; +const REMOVE_FILTER = 'removeFilter'; + +const includeFiltersExpressions = [FILTERS, SELECT_FILTER]; +const excludeFiltersExpressions = [REMOVE_FILTER]; +const filtersExpressions = [...includeFiltersExpressions, ...excludeFiltersExpressions]; + export const defaultFormatter = (value: unknown) => (value || null ? `${value}` : '-'); export const formatFilterView = @@ -55,41 +63,73 @@ export const groupFiltersBy = (filters: FilterType[], groupByField: FilterField) })); }; -export const getFiltersByGroups = ( - filters: string[], - groups: string[], - ungrouped: boolean = false -) => - filters.filter((filter: string) => { - const ast = fromExpression(filter); - const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); - if (!groups?.length && ungrouped) { - return expGroups.length === 0; - } +const excludeFiltersByGroups = (filters: Ast[], filterExprAst: ExpressionFunctionAST) => { + const groupsToExclude = filterExprAst.arguments.group ?? []; + const removeUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; + return filters.filter((filter) => { + const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + (group: string) => group !== '' + ); + const noNeedToExcludeByGroup = !( + groups.length && + groupsToExclude.length && + groupsToExclude.includes(groups[0]) + ); + + const noNeedToExcludeByUngrouped = (removeUngrouped && groups.length) || !removeUngrouped; + const excludeAllFilters = !groupsToExclude.length && !removeUngrouped; - return ( - !groups.length || - (expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup))) + return !excludeAllFilters && noNeedToExcludeByUngrouped && noNeedToExcludeByGroup; + }); +}; + +const includeFiltersByGroups = ( + filters: Ast[], + filterExprAst: ExpressionFunctionAST, + ignoreUngroupedIfGroups: boolean = false +) => { + const groupsToInclude = filterExprAst.arguments.group ?? []; + const includeOnlyUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; + return filters.filter((filter) => { + const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + (group: string) => group !== '' ); + const needToIncludeByGroup = + groups.length && groupsToInclude.length && groupsToInclude.includes(groups[0]); + + const needToIncludeByUngrouped = + includeOnlyUngrouped && + !groups.length && + (ignoreUngroupedIfGroups ? !groupsToInclude.length : true); + + const allowAll = !groupsToInclude.length && !includeOnlyUngrouped; + return needToIncludeByUngrouped || needToIncludeByGroup || allowAll; }); +}; -export const extractGroupsFromElementsFilters = (expr: string) => { - const ast = fromExpression(expr); - const filtersFns = ast.chain.filter((expression) => expression.function === 'filters'); - const groups = filtersFns.reduce((foundGroups, filterFn) => { - const filterGroups = filterFn?.arguments.group?.map((g) => g.toString()) ?? []; - return [...foundGroups, ...filterGroups]; - }, []); - return [...new Set(groups)]; +export const getFiltersByFilterExpressions = ( + filters: string[], + filterExprsAsts: ExpressionFunctionAST[] +) => { + const filtersAst = filters.map((filter) => fromExpression(filter)); + const matchedFiltersAst = filterExprsAsts.reduce((includedFilters, filter) => { + if (excludeFiltersExpressions.includes(filter.function)) { + return excludeFiltersByGroups(includedFilters, filter); + } + const isFiltersExpr = filter.function === FILTERS; + const filtersToInclude = isFiltersExpr ? filtersAst : includedFilters; + return includeFiltersByGroups(filtersToInclude, filter, isFiltersExpr); + }, filtersAst); + + return matchedFiltersAst.map((ast) => toExpression(ast)); }; -export const extractUngroupedFromElementsFilters = (expr: string) => { +export const getFiltersExprsFromExpression = (expr: string) => { const ast = fromExpression(expr); - const filtersFns = ast.chain.filter((expression) => expression.function === 'filters'); - return filtersFns.some((filterFn) => filterFn?.arguments.ungrouped?.[0]); + return ast.chain.filter((expression) => filtersExpressions.includes(expression.function)); }; export const isExpressionWithFilters = (expr: string) => { const ast = fromExpression(expr); - return ast.chain.some((expression) => expression.function === 'filters'); + return ast.chain.some((expression) => filtersExpressions.includes(expression.function)); }; diff --git a/x-pack/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/plugins/canvas/public/lib/run_interpreter.ts deleted file mode 100644 index 77c31b11924c0..0000000000000 --- a/x-pack/plugins/canvas/public/lib/run_interpreter.ts +++ /dev/null @@ -1,72 +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 { fromExpression, getType } from '@kbn/interpreter'; -import { pluck } from 'rxjs/operators'; -import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; -import { pluginServices } from '../services'; - -interface Options { - castToRender?: boolean; -} - -/** - * Meant to be a replacement for plugins/interpreter/interpretAST - */ -export async function interpretAst( - ast: ExpressionAstExpression, - variables: Record, - input: ExpressionValue = null -): Promise { - const context = { variables }; - const { execute } = pluginServices.getServices().expressions; - - return await execute(ast, input, context).getData().pipe(pluck('result')).toPromise(); -} - -/** - * Runs interpreter, usually in the browser - * - * @param {object} ast - Executable AST - * @param {any} input - Initial input for AST execution - * @param {object} variables - Variables to pass in to the intrepreter context - * @param {object} options - * @param {boolean} options.castToRender - try to cast to a type: render object? - * @returns {promise} - */ -export async function runInterpreter( - ast: ExpressionAstExpression, - input: ExpressionValue, - variables: Record, - options: Options = {} -): Promise { - const context = { variables }; - try { - const { execute } = pluginServices.getServices().expressions; - - const renderable = await execute(ast, input, context) - .getData() - .pipe(pluck('result')) - .toPromise(); - - if (getType(renderable) === 'render') { - return renderable; - } - - if (options.castToRender) { - return runInterpreter(fromExpression('render'), renderable, variables, { - castToRender: false, - }); - } - - throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); - } catch (err) { - const { error: displayError } = pluginServices.getServices().notify; - displayError(err); - throw err; - } -} diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 8cdc695ebaaba..1c2ce763f42e2 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -36,7 +36,6 @@ import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; import { setupExpressions } from './setup_expressions'; -import { pluginServiceRegistry } from './services/kibana'; export type { CoreStart, CoreSetup }; @@ -123,6 +122,8 @@ export class CanvasPlugin srcPlugin.start(coreStart, startPlugins); const { pluginServices } = await import('./services'); + const { pluginServiceRegistry } = await import('./services/kibana'); + pluginServices.setRegistry( pluginServiceRegistry.start({ coreStart, diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index 01bb0adb17711..456a1314bdfff 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -5,6 +5,4 @@ * 2.0. */ -import { ExpressionsServiceStart } from '../../../../../src/plugins/expressions/public'; - -export type CanvasExpressionsService = ExpressionsServiceStart; +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 new file mode 100644 index 0000000000000..1ced3d15f6e10 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/filters.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 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 ed55f919e4c76..4bf025c274859 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -12,6 +12,7 @@ import { PluginServices } from '../../../../../src/plugins/presentation_util/pub import { CanvasCustomElementService } from './custom_element'; 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'; @@ -24,6 +25,7 @@ export interface CanvasPluginServices { customElement: CanvasCustomElementService; embeddables: CanvasEmbeddablesService; expressions: CanvasExpressionsService; + filters: CanvasFiltersService; labs: CanvasLabsService; navLink: CanvasNavLinkService; notify: CanvasNotifyService; @@ -41,6 +43,7 @@ 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())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/expressions.ts b/x-pack/plugins/canvas/public/services/kibana/expressions.ts index 780de5309d97e..ea329b63863f8 100644 --- a/x-pack/plugins/canvas/public/services/kibana/expressions.ts +++ b/x-pack/plugins/canvas/public/services/kibana/expressions.ts @@ -4,16 +4,137 @@ * 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 'src/plugins/expressions'; +import { pluck } from 'rxjs/operators'; +import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; +import { ExpressionsServiceStart } from '../../../../../../src/plugins/expressions/public'; import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; - import { CanvasStartDeps } from '../../plugin'; -import { CanvasExpressionsService } from '../expressions'; +import { CanvasFiltersService } from './filters'; +import { CanvasNotifyService } from '../notify'; + +interface Options { + castToRender?: boolean; +} + +export class ExpressionsService { + private filters: CanvasFiltersService; + private notify: CanvasNotifyService; + + constructor( + private readonly expressions: ExpressionsServiceStart, + { filters, notify }: CanvasExpressionsServiceRequiredServices + ) { + this.filters = filters; + this.notify = notify; + } + + async interpretAst( + ast: ExpressionAstExpression, + variables: Record, + input: ExpressionValue = null + ) { + const context = await this.getGlobalContext(); + return await this.interpretAstWithContext(ast, input, { + ...(context ?? {}), + variables, + }); + } + + async interpretAstWithContext( + ast: ExpressionAstExpression, + input: ExpressionValue = null, + context?: ExpressionExecutionParams + ): Promise { + return await this.expressions + .execute(ast, input, context) + .getData() + .pipe(pluck('result')) + .toPromise(); + } + + /** + * Runs interpreter, usually in the browser + * + * @param {object} ast - Executable AST + * @param {any} input - Initial input for AST execution + * @param {object} variables - Variables to pass in to the intrepreter context + * @param {object} options + * @param {boolean} options.castToRender - try to cast to a type: render object? + * @returns {Promise} + */ + async runInterpreter( + ast: ExpressionAstExpression, + input: ExpressionValue, + variables: Record, + options: Options = {} + ): Promise { + const context = await this.getGlobalContext(); + const fullContext = { ...(context ?? {}), variables }; + + try { + const renderable = await this.interpretAstWithContext(ast, input, fullContext); + + if (getType(renderable) === 'render') { + return renderable; + } + + if (options.castToRender) { + return this.runInterpreter(fromExpression('render'), renderable, fullContext, { + castToRender: false, + }); + } + + throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); + } catch (err) { + this.notify.error(err); + throw err; + } + } + + getRenderer(name: string) { + return this.expressions.getRenderer(name); + } + + getFunctions() { + return this.expressions.getFunctions(); + } + + private async getFilters() { + const filtersList = this.filters.getFilters(); + const context = this.filters.getFiltersContext(); + const filterExpression = filtersList.join(' | '); + const filterAST = fromExpression(filterExpression); + return await this.interpretAstWithContext(filterAST, null, context); + } + + private async getGlobalContext() { + const canvasFilters = await this.getFilters(); + const kibanaFilters = buildEmbeddableFilters(canvasFilters ? canvasFilters.and : []); + return { + searchContext: { ...kibanaFilters }, + }; + } +} + +export type CanvasExpressionsService = ExpressionsService; +export interface CanvasExpressionsServiceRequiredServices { + notify: CanvasNotifyService; + filters: CanvasFiltersService; +} export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory< CanvasExpressionsService, - CanvasStartDeps + CanvasStartDeps, + CanvasExpressionsServiceRequiredServices >; -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ({ startPlugins }) => - startPlugins.expressions; +export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( + { startPlugins }, + requiredServices +) => new ExpressionsService(startPlugins.expressions, requiredServices); diff --git a/x-pack/plugins/canvas/public/services/kibana/filters.ts b/x-pack/plugins/canvas/public/services/kibana/filters.ts new file mode 100644 index 0000000000000..872b6759b389b --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/filters.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 { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/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'; +// @ts-expect-error untyped local +import { setFilter } from '../../state/actions/elements'; + +export class FiltersService { + constructor() {} + + getFilters(state: State = getState()) { + return getGlobalFilters(state); + } + + updateFilter(filterId: string, filterExpression: string) { + const { dispatch } = getStore(); + dispatch(setFilter(filterExpression, filterId, true)); + } + + getFiltersContext(state: State = getState()) { + const variables = getWorkpadVariablesAsObject(state); + return { variables }; + } +} + +export type CanvasFiltersService = FiltersService; + +export type CanvasFiltersServiceFactory = KibanaPluginServiceFactory< + CanvasFiltersService, + CanvasStartDeps +>; + +export const filtersServiceFactory: CanvasFiltersServiceFactory = () => new FiltersService(); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 91767947bc0a6..c1ceb531657d0 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -24,10 +24,12 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; +import { filtersServiceFactory } from './filters'; export { customElementServiceFactory } from './custom_element'; export { embeddablesServiceFactory } from './embeddables'; export { expressionsServiceFactory } from './expressions'; +export { filtersServiceFactory } from './filters'; export { labsServiceFactory } from './labs'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; @@ -41,7 +43,8 @@ export const pluginServiceProviders: PluginServiceProviders< > = { customElement: new PluginServiceProvider(customElementServiceFactory), embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory), + expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), + filters: new PluginServiceProvider(filtersServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), diff --git a/x-pack/plugins/canvas/public/services/kibana/workpad.ts b/x-pack/plugins/canvas/public/services/kibana/workpad.ts index 9f69d5096237c..c0ef1097555a6 100644 --- a/x-pack/plugins/canvas/public/services/kibana/workpad.ts +++ b/x-pack/plugins/canvas/public/services/kibana/workpad.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SavedObject } from 'kibana/public'; import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; import { CanvasStartDeps } from '../../plugin'; @@ -67,6 +68,21 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad }; }, + export: async (id: string) => { + const workpad = await coreStart.http.get>( + `${getApiPath()}/export/${id}` + ); + const { attributes } = workpad; + + return { + ...workpad, + attributes: { + ...attributes, + css: attributes.css ?? DEFAULT_WORKPAD_CSS, + variables: attributes.variables ?? [], + }, + }; + }, resolve: async (id: string) => { const { workpad, outcome, aliasId } = await coreStart.http.get( `${getApiPath()}/resolve/${id}` @@ -93,6 +109,14 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, }), }); }, + import: (workpad: CanvasWorkpad) => + coreStart.http.post(`${getApiPath()}/import`, { + body: JSON.stringify({ + ...sanitizeWorkpad({ ...workpad }), + assets: workpad.assets || {}, + variables: workpad.variables || [], + }), + }), createFromTemplate: (templateId: string) => { return coreStart.http.post(getApiPath(), { body: JSON.stringify({ templateId }), diff --git a/x-pack/plugins/canvas/public/services/storybook/workpad.ts b/x-pack/plugins/canvas/public/services/storybook/workpad.ts index 6c77bdb1adeac..5dd40997900c6 100644 --- a/x-pack/plugins/canvas/public/services/storybook/workpad.ts +++ b/x-pack/plugins/canvas/public/services/storybook/workpad.ts @@ -31,7 +31,7 @@ type CanvasWorkpadServiceFactory = PluginServiceFactory () => new Promise((resolve) => setTimeout(resolve, time)); -const { findNoTemplates, findNoWorkpads, findSomeTemplates } = stubs; +const { findNoTemplates, findNoWorkpads, findSomeTemplates, importWorkpad } = stubs; const getRandomName = () => { const lorem = @@ -85,6 +85,10 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ 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); diff --git a/x-pack/plugins/canvas/public/services/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/stubs/expressions.ts index 6660c1c6efb35..405f2ebe0ba91 100644 --- a/x-pack/plugins/canvas/public/services/stubs/expressions.ts +++ b/x-pack/plugins/canvas/public/services/stubs/expressions.ts @@ -10,11 +10,22 @@ import { plugin } from '../../../../../../src/plugins/expressions/public'; import { functions as functionDefinitions } from '../../../canvas_plugin_src/functions/common'; import { renderFunctions } from '../../../canvas_plugin_src/renderers/core'; import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; -import { CanvasExpressionsService } from '../expressions'; +import { + CanvasExpressionsService, + CanvasExpressionsServiceRequiredServices, + ExpressionsService, +} from '../kibana/expressions'; -type CanvasExpressionsServiceFactory = PluginServiceFactory; +type CanvasExpressionsServiceFactory = PluginServiceFactory< + CanvasExpressionsService, + {}, + CanvasExpressionsServiceRequiredServices +>; -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = () => { +export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( + params, + requiredServices +) => { const placeholder = {} as any; const expressionsPlugin = plugin(placeholder); const setup = expressionsPlugin.setup(placeholder); @@ -25,5 +36,5 @@ export const expressionsServiceFactory: CanvasExpressionsServiceFactory = () => expressionsService.registerRenderer(fn as unknown as AnyExpressionRenderDefinition); }); - return expressionsService; + return new ExpressionsService(expressionsService, requiredServices); }; diff --git a/x-pack/plugins/canvas/public/services/stubs/filters.ts b/x-pack/plugins/canvas/public/services/stubs/filters.ts new file mode 100644 index 0000000000000..972dbfd6dc0e4 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/filters.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/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 index 2216013a29c12..d90b1a3c92201 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -24,9 +24,11 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; +import { filtersServiceFactory } from './filters'; export { customElementServiceFactory } from './custom_element'; export { expressionsServiceFactory } from './expressions'; +export { filtersServiceFactory } from './filters'; export { labsServiceFactory } from './labs'; export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; @@ -38,7 +40,8 @@ export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { customElement: new PluginServiceProvider(customElementServiceFactory), embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory), + expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), + filters: new PluginServiceProvider(filtersServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts index c10244038750d..6268fa128df0f 100644 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -6,13 +6,12 @@ */ import moment from 'moment'; - import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; // @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; +import { getDefaultWorkpad, getExportedWorkpad } from '../../state/defaults'; import { CanvasWorkpadService } from '../workpad'; -import { CanvasTemplate } from '../../../types'; +import { CanvasTemplate, CanvasWorkpad } from '../../../types'; type CanvasWorkpadServiceFactory = PluginServiceFactory; @@ -94,6 +93,7 @@ export const findNoTemplates = .then(() => getNoTemplates()); }; +export const importWorkpad = (workpad: CanvasWorkpad) => Promise.resolve(workpad); export const getNoTemplates = () => ({ templates: [] }); export const getSomeTemplates = () => ({ templates }); @@ -103,6 +103,7 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({ 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(), diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index 8e77ab3f321ef..233b1a70ff7f6 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -25,10 +25,12 @@ export interface ResolveWorkpadResponse { outcome: SavedObjectsResolveResponse['outcome']; aliasId?: SavedObjectsResolveResponse['alias_target_id']; } + 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; diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index bcc02c3cbc2cd..72186abd38c94 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -20,7 +20,6 @@ import { import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; import { ErrorStrings } from '../../../i18n'; -import { runInterpreter, interpretAst } from '../../lib/run_interpreter'; import { subMultitree } from '../../lib/aeroelastic/functional'; import { pluginServices } from '../../services'; import { selectToplevelNodes } from './transient'; @@ -101,11 +100,16 @@ export const fetchContext = createThunk( }); const variables = getWorkpadVariablesAsObject(getState()); + + const { expressions } = pluginServices.getServices(); const elementWithNewAst = set(element, pathToTarget, astChain); + // get context data from a partial AST - return interpretAst(elementWithNewAst.ast, variables, prevContextValue).then((value) => { - dispatch(args.setValue({ path: contextPath, value })); - }); + return expressions + .interpretAst(elementWithNewAst.ast, variables, prevContextValue) + .then((value) => { + dispatch(args.setValue({ path: contextPath, value })); + }); } ); @@ -124,14 +128,14 @@ const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, cont }); const variables = getWorkpadVariablesAsObject(getState()); - - return runInterpreter(ast, context, variables, { castToRender: true }) + const { expressions, notify } = pluginServices.getServices(); + return expressions + .runInterpreter(ast, context, variables, { castToRender: true }) .then((renderable) => { dispatch(getAction(renderable)); }) .catch((err) => { - const notifyService = pluginServices.getServices().notify; - notifyService.error(err); + notify.error(err); dispatch(getAction(err)); }); }; @@ -171,12 +175,13 @@ export const fetchAllRenderables = createThunk( const argumentPath = [element.id, 'expressionRenderable']; const variables = getWorkpadVariablesAsObject(getState()); + const { expressions, notify } = pluginServices.getServices(); - return runInterpreter(ast, null, variables, { castToRender: true }) + return expressions + .runInterpreter(ast, null, variables, { castToRender: true }) .then((renderable) => ({ path: argumentPath, value: renderable })) .catch((err) => { - const notifyService = pluginServices.getServices().notify; - notifyService.error(err); + notify.error(err); return { path: argumentPath, value: err }; }); }); diff --git a/x-pack/plugins/canvas/public/state/defaults.js b/x-pack/plugins/canvas/public/state/defaults.js index 40e8425c98ff0..a4a38d50388d5 100644 --- a/x-pack/plugins/canvas/public/state/defaults.js +++ b/x-pack/plugins/canvas/public/state/defaults.js @@ -87,6 +87,14 @@ export const getDefaultWorkpad = () => { }; }; +export const getExportedWorkpad = () => { + const workpad = getDefaultWorkpad(); + return { + id: workpad.id, + attributes: workpad, + }; +}; + export const getDefaultSidebar = () => ({ groupFiltersByOption: DEFAULT_GROUP_BY_FIELD, }); diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index ac94ccc562e88..557a6b8acc4e7 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -27,6 +27,7 @@ import { ExpressionAstFunction, ExpressionAstExpression, } from '../../../types'; +import { isExpressionWithFilters } from '../../lib/filter'; type Modify = Pick> & R; type WorkpadInfo = Modify; @@ -248,7 +249,7 @@ function extractFilterGroups( // TODO: we always get a function here, right? const { function: fn, arguments: args } = item; - if (fn === 'filters') { + if (isExpressionWithFilters(fn)) { // we have a filter function, extract groups from args return groups.concat( buildGroupValues(args, (argValue) => { diff --git a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts index 216cdc0970dc4..13e4e34b20b66 100644 --- a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts @@ -12,6 +12,7 @@ export interface MockWorkpadRouteContext extends CanvasRouteHandlerContext { workpad: { create: jest.Mock; get: jest.Mock; + import: jest.Mock; update: jest.Mock; resolve: jest.Mock; }; @@ -23,6 +24,7 @@ export const workpadRouteContextMock = { workpad: { create: jest.fn(), get: jest.fn(), + import: jest.fn(), update: jest.fn(), resolve: jest.fn(), }, diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index ebe43ba76a46a..27b6186216b69 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -23,7 +23,8 @@ import { initRoutes } from './routes'; import { registerCanvasUsageCollector } from './collectors'; import { loadSampleData } from './sample_data'; import { setupInterpreter } from './setup_interpreter'; -import { customElementType, workpadType, workpadTemplateType } from './saved_objects'; +import { customElementType, workpadTypeFactory, workpadTemplateType } from './saved_objects'; +import type { CanvasSavedObjectTypeMigrationsDeps } from './saved_objects/migrations'; import { initializeTemplates } from './templates'; import { essqlSearchStrategyProvider } from './lib/essql_strategy'; import { getUISettings } from './ui_settings'; @@ -53,10 +54,18 @@ export class CanvasPlugin implements Plugin { public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { const expressionsFork = plugins.expressions.fork(); + setupInterpreter(expressionsFork, { + embeddablePersistableStateService: { + extract: plugins.embeddable.extract, + inject: plugins.embeddable.inject, + }, + }); + + const deps: CanvasSavedObjectTypeMigrationsDeps = { expressions: expressionsFork }; coreSetup.uiSettings.register(getUISettings()); - coreSetup.savedObjects.registerType(customElementType); - coreSetup.savedObjects.registerType(workpadType); - coreSetup.savedObjects.registerType(workpadTemplateType); + coreSetup.savedObjects.registerType(customElementType(deps)); + coreSetup.savedObjects.registerType(workpadTypeFactory(deps)); + coreSetup.savedObjects.registerType(workpadTemplateType(deps)); plugins.features.registerKibanaFeature(getCanvasFeature(plugins)); @@ -84,13 +93,6 @@ export class CanvasPlugin implements Plugin { const kibanaIndex = coreSetup.savedObjects.getKibanaIndex(); registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex); - setupInterpreter(expressionsFork, { - embeddablePersistableStateService: { - extract: plugins.embeddable.extract, - inject: plugins.embeddable.inject, - }, - }); - coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); plugins.data.search.registerSearchStrategy(ESSQL_SEARCH_STRATEGY, strategy); diff --git a/x-pack/plugins/canvas/server/routes/workpad/import.ts b/x-pack/plugins/canvas/server/routes/workpad/import.ts new file mode 100644 index 0000000000000..35d362f43becc --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/import.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 { RouteInitializerDeps } from '../'; +import { API_ROUTE_WORKPAD_IMPORT } from '../../../common/lib/constants'; +import { ImportedCanvasWorkpad } from '../../../types'; +import { ImportedWorkpadSchema } from './workpad_schema'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +const createRequestBodySchema = ImportedWorkpadSchema; + +export function initializeImportWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD_IMPORT}`, + validate: { + body: createRequestBodySchema, + }, + options: { + body: { + maxBytes: 26214400, + accepts: ['application/json'], + }, + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = request.body as ImportedCanvasWorkpad; + + const createdObject = await context.canvas.workpad.import(workpad); + + return response.ok({ + body: { ...okResponse, id: createdObject.id }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts index 8483642e59c5a..b97d58ee232f1 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/index.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -9,6 +9,7 @@ import { RouteInitializerDeps } from '../'; import { initializeFindWorkpadsRoute } from './find'; import { initializeGetWorkpadRoute } from './get'; import { initializeCreateWorkpadRoute } from './create'; +import { initializeImportWorkpadRoute } from './import'; import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; import { initializeDeleteWorkpadRoute } from './delete'; import { initializeResolveWorkpadRoute } from './resolve'; @@ -18,6 +19,7 @@ export function initWorkpadRoutes(deps: RouteInitializerDeps) { initializeResolveWorkpadRoute(deps); initializeGetWorkpadRoute(deps); initializeCreateWorkpadRoute(deps); + initializeImportWorkpadRoute(deps); initializeUpdateWorkpadRoute(deps); initializeUpdateWorkpadAssetsRoute(deps); initializeDeleteWorkpadRoute(deps); diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts index 9bde26298185b..473b46d470265 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { TypeOf, schema } from '@kbn/config-schema'; export const PositionSchema = schema.object({ angle: schema.number(), @@ -18,30 +18,30 @@ export const PositionSchema = schema.object({ export const WorkpadElementSchema = schema.object({ expression: schema.string(), - filter: schema.maybe(schema.nullable(schema.string())), + filter: schema.nullable(schema.string({ defaultValue: '' })), id: schema.string(), position: PositionSchema, }); export const WorkpadPageSchema = schema.object({ elements: schema.arrayOf(WorkpadElementSchema), - groups: schema.maybe( - schema.arrayOf( - schema.object({ - id: schema.string(), - position: PositionSchema, - }) - ) + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }), + { defaultValue: [] } ), id: schema.string(), style: schema.recordOf(schema.string(), schema.string()), - transition: schema.maybe( - schema.oneOf([ - schema.object({}), + transition: schema.oneOf( + [ + schema.object({}, { defaultValue: {} }), schema.object({ name: schema.string(), }), - ]) + ], + { defaultValue: {} } ), }); @@ -55,44 +55,71 @@ export const WorkpadAssetSchema = schema.object({ export const WorkpadVariable = schema.object({ name: schema.string(), value: schema.oneOf([schema.string(), schema.number(), schema.boolean()]), - type: schema.string(), + type: schema.string({ + validate: (type) => { + const validTypes = ['string', 'number', 'boolean']; + if (type && !validTypes.includes(type)) { + return `${type} is invalid type for a variable. Valid types: ${validTypes.join(', ')}.`; + } + }, + }), }); -export const WorkpadSchema = schema.object( - { - '@created': schema.maybe(schema.string()), - '@timestamp': schema.maybe(schema.string()), - assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), - colors: schema.arrayOf(schema.string()), - css: schema.string(), - variables: schema.arrayOf(WorkpadVariable), - height: schema.number(), - id: schema.string(), - isWriteable: schema.maybe(schema.boolean()), - name: schema.string(), - page: schema.number(), - pages: schema.arrayOf(WorkpadPageSchema), - width: schema.number(), - }, - { - validate: (workpad) => { - // Validate unique page ids - const pageIdsArray = workpad.pages.map((page) => page.id); - const pageIdsSet = new Set(pageIdsArray); +const commonWorkpadFields = { + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + variables: schema.arrayOf(WorkpadVariable), + height: schema.number(), + id: schema.maybe(schema.string()), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +}; - if (pageIdsArray.length !== pageIdsSet.size) { - return 'Page Ids are not unique'; - } +const WorkpadSchemaWithoutValidation = schema.object({ + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + ...commonWorkpadFields, +}); - // Validate unique element ids - const elementIdsArray = workpad.pages - .map((page) => page.elements.map((element) => element.id)) - .flat(); - const elementIdsSet = new Set(elementIdsArray); +const ImportedWorkpadSchemaWithoutValidation = schema.object({ + assets: schema.recordOf(schema.string(), WorkpadAssetSchema), + ...commonWorkpadFields, +}); - if (elementIdsArray.length !== elementIdsSet.size) { - return 'Element Ids are not unique'; - } - }, +const validate = (workpad: TypeOf) => { + // Validate unique page ids + const pageIdsArray = workpad.pages.map((page) => page.id); + const pageIdsSet = new Set(pageIdsArray); + + if (pageIdsArray.length !== pageIdsSet.size) { + return 'Page Ids are not unique'; + } + + // Validate unique element ids + const elementIdsArray = workpad.pages + .map((page) => page.elements.map((element) => element.id)) + .flat(); + const elementIdsSet = new Set(elementIdsArray); + + if (elementIdsArray.length !== elementIdsSet.size) { + return 'Element Ids are not unique'; + } +}; + +export const WorkpadSchema = WorkpadSchemaWithoutValidation.extends( + {}, + { + validate, + } +); + +export const ImportedWorkpadSchema = ImportedWorkpadSchemaWithoutValidation.extends( + {}, + { + validate, } ); diff --git a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts index d62642f5619ea..82305b2fdd95f 100644 --- a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts +++ b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts @@ -7,8 +7,9 @@ import { SavedObjectsType } from 'src/core/server'; import { CUSTOM_ELEMENT_TYPE } from '../../common/lib/constants'; +import { customElementMigrationsFactory, CanvasSavedObjectTypeMigrationsDeps } from './migrations'; -export const customElementType: SavedObjectsType = { +export const customElementType = (deps: CanvasSavedObjectTypeMigrationsDeps): SavedObjectsType => ({ name: CUSTOM_ELEMENT_TYPE, hidden: false, namespaceType: 'multiple-isolated', @@ -31,7 +32,7 @@ export const customElementType: SavedObjectsType = { '@created': { type: 'date' }, }, }, - migrations: {}, + migrations: customElementMigrationsFactory(deps), management: { icon: 'canvasApp', defaultSearchField: 'name', @@ -40,4 +41,4 @@ export const customElementType: SavedObjectsType = { return obj.attributes.displayName; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/saved_objects/index.ts b/x-pack/plugins/canvas/server/saved_objects/index.ts index dfc27c4b6fa66..9e7cd8644c7c7 100644 --- a/x-pack/plugins/canvas/server/saved_objects/index.ts +++ b/x-pack/plugins/canvas/server/saved_objects/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { workpadType } from './workpad'; +import { workpadTypeFactory } from './workpad'; import { customElementType } from './custom_element'; import { workpadTemplateType } from './workpad_template'; -export { customElementType, workpadType, workpadTemplateType }; +export { customElementType, workpadTypeFactory, workpadTemplateType }; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts new file mode 100644 index 0000000000000..20eba14c5cff0 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts @@ -0,0 +1,145 @@ +/* + * Copyright 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 { Ast, fromExpression, toExpression } from '@kbn/interpreter'; +import { Serializable } from '@kbn/utility-types'; +import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { flowRight, mapValues } from 'lodash'; +import { + CanvasElement, + CanvasTemplateElement, + CanvasTemplate, + CustomElement, + CustomElementContent, + CustomElementNode, +} from '../../../types'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; +import { WorkpadAttributes } from '../../routes/workpad/workpad_attributes'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; + +type ToSerializable = { + [K in keyof Type]: Type[K] extends unknown[] + ? ToSerializable + : Type[K] extends {} + ? ToSerializable + : Serializable; +}; + +type ExprAst = ToSerializable; + +interface CommonPage { + elements?: T[]; +} +interface CommonWorkpad, U> { + pages?: T[]; +} + +type MigrationFn = ( + migrate: MigrateFunction, + version: string +) => SavedObjectMigrationFn; + +const toAst = (expression: string): ExprAst => fromExpression(expression); +const fromAst = (ast: Ast): string => toExpression(ast); + +const migrateExpr = (expr: string, migrateFn: MigrateFunction) => + flowRight(fromAst, migrateFn, toAst)(expr); + +const migrateWorkpadElement = + (migrate: MigrateFunction) => + ({ filter, expression, ...element }: CanvasElement | CustomElementNode) => ({ + ...element, + filter: filter ? migrateExpr(filter, migrate) : filter, + expression: expression ? migrateExpr(expression, migrate) : expression, + }); + +const migrateTemplateElement = + (migrate: MigrateFunction) => + ({ expression, ...element }: CanvasTemplateElement) => ({ + ...element, + expression: expression ? migrateExpr(expression, migrate) : expression, + }); + +const migrateWorkpadElements = , U>( + doc: SavedObjectUnsanitizedDoc | undefined>, + migrateElementFn: any +) => { + if ( + typeof doc.attributes !== 'object' || + doc.attributes === null || + doc.attributes === undefined + ) { + return doc; + } + + const { pages } = doc.attributes; + + const newPages = pages?.map((page) => { + const { elements } = page; + const newElements = elements?.map(migrateElementFn); + return { ...page, elements: newElements }; + }); + + return { ...doc, attributes: { ...doc.attributes, pages: newPages } }; +}; + +const migrateTemplateWorkpadExpressions: MigrationFn = + (migrate) => (doc) => + migrateWorkpadElements(doc, migrateTemplateElement(migrate)); + +const migrateWorkpadExpressionsAndFilters: MigrationFn = (migrate) => (doc) => + migrateWorkpadElements(doc, migrateWorkpadElement(migrate)); + +const migrateCustomElementExpressionsAndFilters: MigrationFn = + (migrate) => (doc) => { + if ( + typeof doc.attributes !== 'object' || + doc.attributes === null || + doc.attributes === undefined + ) { + return doc; + } + + const { content } = doc.attributes; + const { selectedNodes = [] }: CustomElementContent = content + ? JSON.parse(content) + : { selectedNodes: [] }; + + const newSelectedNodes = selectedNodes.map((element) => { + const newElement = migrateWorkpadElement(migrate)(element); + return { ...element, ...newElement, ast: toAst(newElement.expression) }; + }); + + const newContent = JSON.stringify({ selectedNodes: newSelectedNodes }); + return { ...doc, attributes: { ...doc.attributes, content: newContent } }; + }; + +export const workpadExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateWorkpadExpressionsAndFilters + ) as MigrateFunctionsObject; + +export const templateWorkpadExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateTemplateWorkpadExpressions + ) as MigrateFunctionsObject; + +export const customElementExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateCustomElementExpressionsAndFilters + ) as MigrateFunctionsObject; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts new file mode 100644 index 0000000000000..88913b50c3c4c --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + customElementExpressionsMigrationsFactory, + templateWorkpadExpressionsMigrationsFactory, + workpadExpressionsMigrationsFactory, +} from './expressions'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; +import { workpadMigrationsFactory as workpadMigrationsFactoryFn } from './workpad'; +import { mergeMigrationFunctionMaps } from '../../../../../../src/plugins/kibana_utils/common'; + +export const workpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + mergeMigrationFunctionMaps( + workpadMigrationsFactoryFn(deps), + workpadExpressionsMigrationsFactory(deps) + ); + +export const templateWorkpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + templateWorkpadExpressionsMigrationsFactory(deps); + +export const customElementMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + customElementExpressionsMigrationsFactory(deps); + +export type { CanvasSavedObjectTypeMigrationsDeps } from './types'; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts index c9c36fd7b26a9..f1dcbd5fe9e7c 100644 --- a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts @@ -7,7 +7,7 @@ import { SavedObjectMigrationFn } from 'src/core/server'; -export const removeAttributesId: SavedObjectMigrationFn = (doc) => { +export const removeAttributesId: SavedObjectMigrationFn = (doc) => { if (typeof doc.attributes === 'object' && doc.attributes !== null) { delete (doc.attributes as any).id; } diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts new file mode 100644 index 0000000000000..18ce0bd88cb69 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ExpressionsService } from 'src/plugins/expressions/public'; + +export interface CanvasSavedObjectTypeMigrationsDeps { + expressions: ExpressionsService; +} diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts new file mode 100644 index 0000000000000..d4d7e2b429711 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common'; +import { removeAttributesId } from './remove_attributes_id'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; + +export const workpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + ({ + '7.0.0': removeAttributesId, + } as unknown as MigrateFunctionsObject); diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/workpad.ts index a8f0f3daf2175..db22025e625e8 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad.ts @@ -7,9 +7,12 @@ import { SavedObjectsType } from 'src/core/server'; import { CANVAS_TYPE } from '../../common/lib/constants'; -import { removeAttributesId } from './migrations/remove_attributes_id'; +import { workpadMigrationsFactory } from './migrations'; +import type { CanvasSavedObjectTypeMigrationsDeps } from './migrations'; -export const workpadType: SavedObjectsType = { +export const workpadTypeFactory = ( + deps: CanvasSavedObjectTypeMigrationsDeps +): SavedObjectsType => ({ name: CANVAS_TYPE, hidden: false, namespaceType: 'multiple-isolated', @@ -29,9 +32,7 @@ export const workpadType: SavedObjectsType = { '@created': { type: 'date' }, }, }, - migrations: { - '7.0.0': removeAttributesId, - }, + migrations: workpadMigrationsFactory(deps), management: { importableAndExportable: true, icon: 'canvasApp', @@ -46,4 +47,4 @@ export const workpadType: SavedObjectsType = { }; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts index eff7f45dcadae..a55c7348c62bb 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts @@ -7,8 +7,14 @@ import { SavedObjectsType } from 'src/core/server'; import { TEMPLATE_TYPE } from '../../common/lib/constants'; +import { + CanvasSavedObjectTypeMigrationsDeps, + templateWorkpadMigrationsFactory, +} from './migrations'; -export const workpadTemplateType: SavedObjectsType = { +export const workpadTemplateType = ( + deps: CanvasSavedObjectTypeMigrationsDeps +): SavedObjectsType => ({ name: TEMPLATE_TYPE, hidden: false, namespaceType: 'agnostic', @@ -44,7 +50,7 @@ export const workpadTemplateType: SavedObjectsType = { }, }, }, - migrations: {}, + migrations: templateWorkpadMigrationsFactory(deps), management: { importableAndExportable: false, icon: 'canvasApp', @@ -53,4 +59,4 @@ export const workpadTemplateType: SavedObjectsType = { return obj.attributes.name; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/templates/pitch_presentation.ts b/x-pack/plugins/canvas/server/templates/pitch_presentation.ts index a9f09ada989c6..82ad535852c97 100644 --- a/x-pack/plugins/canvas/server/templates/pitch_presentation.ts +++ b/x-pack/plugins/canvas/server/templates/pitch_presentation.ts @@ -63,7 +63,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "# Sample."\n| render css=".canvasRenderEl h1 {\ntext-align: center;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "# Sample."\n| render css=".canvasRenderEl h1 {\ntext-align: center;\n}"', }, { id: 'element-33286979-7ea0-41ce-9835-b3bf07f09272', @@ -76,7 +76,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### This is a subtitle"\n| render css=".canvasRenderEl h3 {\ntext-align: center;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### This is a subtitle"\n| render css=".canvasRenderEl h3 {\ntext-align: center;\n}"', }, { id: 'element-1e3b3ffe-4ed8-4376-aad3-77e06d29cafe', @@ -89,7 +89,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "Footnote can go here"\n| render \n css=".canvasRenderEl p {\ntext-align: center;\ncolor: #FFFFFF;\nfont-size: 18px;\nopacity: .7;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "Footnote can go here"\n| render \n css=".canvasRenderEl p {\ntext-align: center;\ncolor: #FFFFFF;\nfont-size: 18px;\nopacity: .7;\n}"', }, { id: 'element-5b5035a3-d5b7-4483-a240-2cf80f5e0acf', @@ -150,7 +150,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render', }, { id: 'element-96a390b6-3d0a-4372-89cb-3ff38eec9565', @@ -162,7 +163,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Half text, half _image._"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Half text, half _image._"\n| render', }, { id: 'element-118b848d-0f89-4d20-868c-21597b7fd5e0', @@ -188,7 +190,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, ], groups: [], @@ -223,7 +225,7 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### BIOS"\n| render', + expression: 'kibana\n| selectFilter\n| demodata\n| markdown "##### BIOS"\n| render', }, { id: 'element-e2c658ee-7614-4d92-a46e-2b1a81a24485', @@ -236,7 +238,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Jane Doe" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Jane Doe" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-3d16765e-5251-4954-8e2a-6c64ed465b73', @@ -249,7 +251,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Developer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Developer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, { id: 'element-624675cf-46e9-4545-b86a-5409bbe53ac1', @@ -262,7 +264,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-dc841809-d2a9-491b-b44f-be92927b8034', @@ -301,7 +303,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-62f241ec-71ce-4edb-a27b-0de990522d20', @@ -314,7 +316,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Designer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Designer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, { id: 'element-aa6c07e0-937f-4362-9d52-f70738faa0c5', @@ -340,7 +342,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## John Smith" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## John Smith" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, ], groups: [], @@ -388,7 +390,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 10"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 10"\n| render', }, { id: 'element-96be0724-0945-4802-8929-1dc456192fb5', @@ -401,7 +404,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Another page style."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Another page style."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', }, { id: 'element-3b4ba0ff-7f95-460e-9fa6-0cbb0f8f3df8', @@ -427,7 +430,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-0b9aa82b-fb0c-4000-805b-146cc9280bc5', @@ -440,7 +443,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Introduction"\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Introduction"\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, ], groups: [], @@ -489,7 +492,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-1ba728f0-f645-4910-9d32-fa5b5820a94c', @@ -502,7 +505,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-db9051eb-7699-4883-b67f-945979cf5650', @@ -528,7 +531,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-fc11525c-2d9c-4a7b-9d96-d54e7bc6479b', @@ -554,7 +557,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-eb9a8883-de47-4a46-9400-b7569f9e69e6', @@ -567,7 +570,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-20c1c86a-658b-4bd2-8326-f987ef84e730', @@ -580,7 +583,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-335db0c3-f678-4cb8-8b93-a6494f1787f5', @@ -593,7 +596,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-079d3cbf-8b15-4ce2-accb-6ba04481019d', @@ -667,7 +670,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render', }, { id: 'element-0f2b9268-f0bd-41b7-abc8-5593276f26fa', @@ -680,7 +684,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Bold title text goes _here_."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Bold title text goes _here_."\n| render', }, { id: 'element-4f4b503e-f1ef-4ab7-aa1d-5d95b3e2e605', @@ -706,7 +710,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-f3f28541-06fe-47ea-89b7-1c5831e28e71', @@ -719,7 +723,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "Caption text goes here" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="right" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "Caption text goes here" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="right" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', }, ], groups: [], @@ -768,7 +772,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-5afa7019-af44-4919-9e11-24e2348cfae9', @@ -781,7 +785,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title for live charts."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title for live charts."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-7b856b52-0d8b-492b-a71f-3508a84388a6', @@ -820,7 +824,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## _Charts with live data._"\n| render css=".canvasRenderEl h1 {\n\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## _Charts with live data._"\n| render css=".canvasRenderEl h1 {\n\n}"', }, { id: 'element-317bed0b-f067-4d2d-8cb4-1145f6e0a11c', @@ -833,7 +837,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-34385617-6eb7-4918-b4db-1a0e8dd6eabe', @@ -846,7 +850,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-b22a35eb-b177-4664-800e-57b91436a879', @@ -859,7 +863,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-651f8a4a-6069-49bf-a7b0-484854628a79', @@ -872,7 +876,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-0ee8c529-4155-442f-8c7c-1df86be37051', @@ -885,7 +889,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-3fb61301-3dc2-411f-ac69-ad22bd37c77d', @@ -898,7 +902,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. \n\nDonec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. \n\nDonec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, ], groups: [], @@ -960,7 +964,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-8b9d3e2b-1d7b-48f4-897c-bf48f0f363d4', @@ -973,7 +977,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title on a _dark_ background."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title on a _dark_ background."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-080c3153-45f7-4efc-8b23-ed7735da426f', @@ -999,7 +1003,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, ], groups: [], @@ -1021,7 +1025,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Bullet point layout style"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Bullet point layout style"\n| render', }, { id: 'element-37dc903a-1c6d-4452-8fc0-38d4afa4631a', @@ -1034,7 +1039,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus\n- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus"\n| render css=".canvasRenderEl li {\nfont-size: 24px;\nline-height: 30px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus\n- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus"\n| render css=".canvasRenderEl li {\nfont-size: 24px;\nline-height: 30px;\n}"', }, { id: 'element-e506de9d-bda1-4018-89bf-f8d02ee5738e', @@ -1047,7 +1052,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', }, { id: 'element-ea5319f5-d204-48c5-a9a0-0724676869a6', @@ -1073,7 +1078,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], @@ -1095,7 +1100,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Paragraph layout style"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Paragraph layout style"\n| render', }, { id: 'element-92b05ab1-c504-4110-a8ad-73d547136024', @@ -1108,7 +1114,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Proin ipsum orci, consectetur a lacus vel, varius rutrum neque. Mauris quis gravida tellus. Integer quis tellus non lectus vestibulum fermentum. Quisque tortor justo, vulputate quis mollis eu, molestie eu ex. Nam eu arcu ac dui mattis facilisis aliquam venenatis est. Quisque tempor risus quis arcu viverra, quis consequat dolor molestie. Sed sed arcu dictum, sollicitudin dui id, iaculis elit. Nunc odio ex, placerat sed hendrerit vitae, finibus eu felis. Sed vulputate mi diam, at dictum mi tempus eu.\n\nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus."\n| render css=".canvasRenderEl p {\nfont-size: 24px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Proin ipsum orci, consectetur a lacus vel, varius rutrum neque. Mauris quis gravida tellus. Integer quis tellus non lectus vestibulum fermentum. Quisque tortor justo, vulputate quis mollis eu, molestie eu ex. Nam eu arcu ac dui mattis facilisis aliquam venenatis est. Quisque tempor risus quis arcu viverra, quis consequat dolor molestie. Sed sed arcu dictum, sollicitudin dui id, iaculis elit. Nunc odio ex, placerat sed hendrerit vitae, finibus eu felis. Sed vulputate mi diam, at dictum mi tempus eu.\n\nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus."\n| render css=".canvasRenderEl p {\nfont-size: 24px;\n}"', }, { id: 'element-e49141ec-3034-4bec-88ca-f9606d12a60a', @@ -1134,7 +1140,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], @@ -1170,7 +1176,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title text can also go _here_ on multiple lines." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title text can also go _here_ on multiple lines." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-a8e0d4b3-864d-4dae-b0dc-64caad06c106', @@ -1196,7 +1202,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, { id: 'element-b54e2908-6908-4dd6-90f1-3ca489807016', @@ -1222,7 +1228,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, { id: 'element-aa54f47c-fecf-4bdb-ac1d-b815d4a8d71d', @@ -1235,7 +1241,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## This title is a _centered_ layout." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## This title is a _centered_ layout." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-6ae072e7-213c-4de9-af22-7fb3e254cf52', @@ -1284,7 +1290,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "## \\"Aliquam mollis auctor nisl vitae varius. Donec nunc turpis, condimentum non sagittis tristique, sollicitudin blandit sem.\\"" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=true}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "## \\"Aliquam mollis auctor nisl vitae varius. Donec nunc turpis, condimentum non sagittis tristique, sollicitudin blandit sem.\\"" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=true}\n| render', }, { id: 'element-989daff8-3571-4e02-b5fc-26657b2d9aaf', @@ -1310,7 +1316,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Lorem Ipsum" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Lorem Ipsum" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, { id: 'element-cf931bd0-e3b6-4ae3-9164-8fe9ba14873d', @@ -1372,7 +1378,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-dc4336d5-9752-421f-8196-9f4a6f8150f0', @@ -1385,7 +1391,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, { id: 'element-b8325cb3-2856-4fd6-8c5a-cba2430dda3e', @@ -1411,7 +1417,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(project)"\n| metric "Projects" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(project)"\n| metric "Projects" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-07f73884-13e9-4a75-8a23-4eb137e75817', @@ -1424,7 +1430,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#FFFFFF" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 16px;\nopacity: .7;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#FFFFFF" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 16px;\nopacity: .7;\n}"', }, { id: 'element-201b8f78-045e-4457-9ada-5166965e64cf', @@ -1437,7 +1443,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, { id: 'element-9b667060-18ba-4f4d-84a2-48adff57efac', @@ -1450,7 +1456,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(country)"\n| metric "Countries" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(country)"\n| metric "Countries" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-23fcecca-1f6a-44f6-b441-0f65e03d8210', @@ -1463,7 +1469,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(username)"\n| metric "Customers" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(username)"\n| metric "Customers" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-19f1db84-7a46-4ccb-a6b9-afd6ddd68523', @@ -1476,7 +1482,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, ], groups: [], @@ -1499,7 +1505,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## An alternative opening title slide."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## An alternative opening title slide."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', }, { id: 'element-433586c1-4d44-40cf-988e-cf51871248fb', @@ -1525,7 +1531,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], diff --git a/x-pack/plugins/canvas/server/workpad_route_context.ts b/x-pack/plugins/canvas/server/workpad_route_context.ts index 9727327fcbd79..d7c818b786e32 100644 --- a/x-pack/plugins/canvas/server/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/workpad_route_context.ts @@ -16,12 +16,13 @@ import { WorkpadAttributes } from './routes/workpad/workpad_attributes'; import { CANVAS_TYPE } from '../common/lib/constants'; import { injectReferences, extractReferences } from './saved_objects/workpad_references'; import { getId } from '../common/lib/get_id'; -import { CanvasWorkpad } from '../types'; +import { CanvasWorkpad, ImportedCanvasWorkpad } from '../types'; export interface CanvasRouteHandlerContext extends RequestHandlerContext { canvas: { workpad: { create: (attributes: CanvasWorkpad) => Promise>; + import: (workpad: ImportedCanvasWorkpad) => Promise>; get: (id: string) => Promise>; resolve: (id: string) => Promise>; update: ( @@ -62,6 +63,33 @@ export const createWorkpadRouteContext: ( { id, references } ); }, + import: async (workpad: ImportedCanvasWorkpad) => { + const now = new Date().toISOString(); + const { id: maybeId, ...workpadWithoutId } = workpad; + + // Functionality of running migrations on import of workpads was implemented in v8.1.0. + // As only attributes of the saved object workpad are exported, to run migrations it is necessary + // to specify the minimal version of possible migrations to execute them. It is v8.0.0 in the current case. + const DEFAULT_MIGRATION_VERSION = { [CANVAS_TYPE]: '8.0.0' }; + const DEFAULT_CORE_MIGRATION_VERSION = '8.0.0'; + + const id = maybeId ? maybeId : getId('workpad'); + + return await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + isWriteable: true, + ...workpadWithoutId, + '@timestamp': now, + '@created': now, + }, + { + migrationVersion: DEFAULT_MIGRATION_VERSION, + coreMigrationVersion: DEFAULT_CORE_MIGRATION_VERSION, + id, + } + ); + }, get: async (id: string) => { const workpad = await context.core.savedObjects.client.get( CANVAS_TYPE, diff --git a/x-pack/plugins/canvas/types/canvas.ts b/x-pack/plugins/canvas/types/canvas.ts index efb121b2948af..09add343aeac4 100644 --- a/x-pack/plugins/canvas/types/canvas.ts +++ b/x-pack/plugins/canvas/types/canvas.ts @@ -66,15 +66,30 @@ export interface CanvasWorkpad { width: number; } -type CanvasTemplateElement = Omit; -type CanvasTemplatePage = Omit & { elements: CanvasTemplateElement[] }; +export type ImportedCanvasWorkpad = Omit< + CanvasWorkpad, + '@created' | '@timestamp' | 'id' | 'isWriteable' +> & { + id?: CanvasWorkpad['id']; + isWriteable?: CanvasWorkpad['isWriteable']; + '@created'?: CanvasWorkpad['@created']; + '@timestamp'?: CanvasWorkpad['@timestamp']; +}; + +export type CanvasTemplateElement = Omit; +export type CanvasTemplatePage = Omit & { + elements: CanvasTemplateElement[]; +}; + export interface CanvasTemplate { id: string; name: string; help: string; tags: string[]; template_key: string; - template?: Omit & { pages: CanvasTemplatePage[] }; + template?: Omit & { + pages: CanvasTemplatePage[] | undefined; + }; } export interface CanvasWorkpadBoundingBox { diff --git a/x-pack/plugins/canvas/types/elements.ts b/x-pack/plugins/canvas/types/elements.ts index 0baf1e086d155..0119c0a842f50 100644 --- a/x-pack/plugins/canvas/types/elements.ts +++ b/x-pack/plugins/canvas/types/elements.ts @@ -49,6 +49,12 @@ export interface CustomElement { content: string; } +export type CustomElementNode = Omit; + +export interface CustomElementContent { + selectedNodes: CustomElementNode[]; +} + export interface ElementPosition { /** * distance from the left edge of the page diff --git a/x-pack/plugins/canvas/types/renderers.ts b/x-pack/plugins/canvas/types/renderers.ts index 2c3931485757d..7c9b785dbb2bb 100644 --- a/x-pack/plugins/canvas/types/renderers.ts +++ b/x-pack/plugins/canvas/types/renderers.ts @@ -26,8 +26,6 @@ export interface CanvasSpecificRendererHandlers { onResize: GenericRendererCallback; /** Handler to invoke when an element should be resized. */ resize: (size: { height: number; width: number }) => void; - /** Sets the value of the filter property on the element object persisted on the workpad */ - setFilter: (filter: string) => void; } export type RendererHandlers = IInterpreterRenderHandlers & CanvasSpecificRendererHandlers; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx index c57283106c6c8..b0f167628496b 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx @@ -40,6 +40,7 @@ const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) {children} diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index e8df35139cec4..f63ce9b3fce88 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -115,7 +115,7 @@ const UserActionMarkdownComponent = forwardRef< /> ) : ( - + {content} ); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx index 4d08994ceb045..5dbcb1cfb9990 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -117,8 +117,9 @@ export function SearchSessionsMgmtTable({ {...props} id={SEARCH_SESSIONS_TABLE_ID} data-test-subj={SEARCH_SESSIONS_TABLE_ID} - rowProps={() => ({ - 'data-test-subj': 'searchSessionsRow', + rowProps={(searchSession: UISession) => ({ + 'data-test-subj': `searchSessionsRow`, + 'data-test-search-session-id': `id-${searchSession.id}`, })} columns={getColumns(core, plugins, api, config, timezone, onActionComplete, kibanaVersion)} items={tableData} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx index 084dcd139f9bf..21fcac7ea6c75 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx @@ -5,22 +5,45 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { Query, IndexPattern, TimefilterContract } from 'src/plugins/data/public'; -import { EuiButton } from '@elastic/eui'; +import { TimefilterContract } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data/common'; + +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiRadioGroup, + EuiRadioGroupOption, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { setFullTimeRange } from './full_time_range_selector_service'; import { useDataVisualizerKibana } from '../../../kibana_context'; +import { DV_FROZEN_TIER_PREFERENCE, useStorage } from '../../hooks/use_storage'; + +export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; interface Props { timefilter: TimefilterContract; - indexPattern: IndexPattern; + indexPattern: DataView; disabled: boolean; - query?: Query; + query?: QueryDslQueryContainer; callback?: (a: any) => void; } +const FROZEN_TIER_PREFERENCE = { + EXCLUDE: 'exclude-frozen', + INCLUDE: 'include-frozen', +} as const; + +type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE]; + // Component for rendering a button which automatically sets the range of the time filter // to the time range of data in the index(es) mapped to the supplied Kibana data view or query. export const FullTimeRangeSelector: FC = ({ @@ -37,36 +60,144 @@ export const FullTimeRangeSelector: FC = ({ } = useDataVisualizerKibana(); // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop - async function setRange(i: IndexPattern, q?: Query) { - try { - const fullTimeRange = await setFullTimeRange(timefilter, i, q); - if (typeof callback === 'function') { - callback(fullTimeRange); + const setRange = useCallback( + async (i: DataView, q?: QueryDslQueryContainer, excludeFrozenData?: boolean) => { + try { + const fullTimeRange = await setFullTimeRange(timefilter, i, q, excludeFrozenData); + if (typeof callback === 'function') { + callback(fullTimeRange); + } + } catch (e) { + toasts.addDanger( + i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification', + { + defaultMessage: 'An error occurred setting the time range.', + } + ) + ); } - } catch (e) { - toasts.addDanger( - i18n.translate( - 'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification', + }, + [callback, timefilter, toasts] + ); + + const [isPopoverOpen, setPopover] = useState(false); + + const [frozenDataPreference, setFrozenDataPreference] = useStorage( + DV_FROZEN_TIER_PREFERENCE, + // By default we will exclude frozen data tier + FROZEN_TIER_PREFERENCE.EXCLUDE + ); + + const setPreference = useCallback( + (id: string) => { + setFrozenDataPreference(id as FrozenTierPreference); + setRange(indexPattern, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE); + closePopover(); + }, + [indexPattern, query, setFrozenDataPreference, setRange] + ); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const sortOptions: EuiRadioGroupOption[] = useMemo(() => { + return [ + { + id: FROZEN_TIER_PREFERENCE.EXCLUDE, + label: i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel', { - defaultMessage: 'An error occurred setting the time range.', + defaultMessage: 'Exclude frozen data tier', } - ) - ); - } - } + ), + }, + { + id: FROZEN_TIER_PREFERENCE.INCLUDE, + label: i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel', + { + defaultMessage: 'Include frozen data tier', + } + ), + }, + ]; + }, []); + + const popoverContent = useMemo( + () => ( + + + + ), + [sortOptions, frozenDataPreference, setPreference] + ); + + const buttonTooltip = useMemo( + () => + frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( + + ) : ( + + ), + [frozenDataPreference] + ); + return ( - setRange(indexPattern, query)} - data-test-subj="dataVisualizerButtonUseFullData" - > - - + + + setRange(indexPattern, query, true)} + data-test-subj="dataVisualizerButtonUseFullData" + > + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downRight" + > + {popoverContent} + + + ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts index f2d14de9812ca..303d54c9d45cc 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts @@ -7,12 +7,14 @@ import moment from 'moment'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Query, TimefilterContract } from 'src/plugins/data/public'; +import { TimefilterContract } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import { getTimeFieldRange } from '../../services/time_field_range'; import { GetTimeFieldRangeResponse } from '../../../../../common/types/time_field_request'; +import { addExcludeFrozenToQuery } from '../../utils/query_utils'; export interface TimeRange { from: number; @@ -22,14 +24,15 @@ export interface TimeRange { export async function setFullTimeRange( timefilter: TimefilterContract, indexPattern: IndexPattern, - query?: Query + query?: QueryDslQueryContainer, + excludeFrozenData?: boolean ): Promise { const runtimeMappings = indexPattern.getComputedFields() .runtimeFields as estypes.MappingRuntimeFields; const resp = await getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, - query, + query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query, ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); timefilter.setTime({ diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts new file mode 100644 index 0000000000000..d6b0bb3322c03 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.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 { useCallback, useState } from 'react'; +import { useDataVisualizerKibana } from '../../kibana_context'; + +export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference'; + +export type DV = Partial<{ + [DV_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen'; +}> | null; + +export type DVKey = keyof Exclude; + +/** + * Hook for accessing and changing a value in the storage. + * @param key - Storage key + * @param initValue + */ +export function useStorage(key: DVKey, initValue?: T): [T, (value: T) => void] { + const { + services: { storage }, + } = useDataVisualizerKibana(); + + const [val, setVal] = useState(storage.get(key) ?? initValue); + + const setStorage = useCallback( + (value: T): void => { + try { + storage.set(key, value); + setVal(value); + } catch (e) { + throw new Error('Unable to update storage with provided value'); + } + }, + [key, storage] + ); + + return [val, setStorage]; +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts index 58a4bd4520829..bcf32a7f62bd7 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts @@ -6,9 +6,9 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { lazyLoadModules } from '../../../lazy_load_bundle'; import { GetTimeFieldRangeResponse } from '../../../../common/types/time_field_request'; -import { Query } from '../../../../../../../src/plugins/data/common/query'; export async function getTimeFieldRange({ index, @@ -18,7 +18,7 @@ export async function getTimeFieldRange({ }: { index: string; timeFieldName?: string; - query?: Query; + query?: QueryDslQueryContainer; runtimeMappings?: estypes.MappingRuntimeFields; }) { const body = JSON.stringify({ index, timeFieldName, query, runtimeMappings }); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts new file mode 100644 index 0000000000000..947b87e9976d5 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.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 { addExcludeFrozenToQuery } from './query_utils'; + +describe('Util: addExcludeFrozenToQuery()', () => { + test('Validation checks.', () => { + expect( + addExcludeFrozenToQuery({ + match_all: {}, + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }) + ).toMatchObject({ + bool: { + must: [{ match_all: {} }], + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: { + term: { + category: { + value: 'clothing', + }, + }, + }, + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: [{ term: { category: { value: 'clothing' } } }], + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect(addExcludeFrozenToQuery(undefined)).toMatchObject({ + bool: { + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + }); +}); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts new file mode 100644 index 0000000000000..43c5d49d1986f --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts @@ -0,0 +1,53 @@ +/* + * Copyright 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { cloneDeep } from 'lodash'; +import { isPopulatedObject } from '../../../../common/utils/object_utils'; + +export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { + const FROZEN_TIER_TERM = { + term: { + _tier: { + value: 'data_frozen', + }, + }, + }; + + if (!originalQuery) { + return { + bool: { + must_not: [FROZEN_TIER_TERM], + }, + }; + } + + const query = cloneDeep(originalQuery); + + delete query.match_all; + + if (isPopulatedObject(query.bool)) { + // Must_not can be both arrays or singular object + if (Array.isArray(query.bool.must_not)) { + query.bool.must_not.push(FROZEN_TIER_TERM); + } else { + // If there's already a must_not condition + if (isPopulatedObject(query.bool.must_not)) { + query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM]; + } + if (query.bool.must_not === undefined) { + query.bool.must_not = [FROZEN_TIER_TERM]; + } + } + } else { + query.bool = { + must_not: [FROZEN_TIER_TERM], + }; + } + + return query; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts index 58d0ac021ff22..83fcc104fbe4b 100644 --- a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts +++ b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts @@ -8,7 +8,11 @@ import { CoreStart } from 'kibana/public'; import { KibanaReactContextValue, useKibana } from '../../../../../src/plugins/kibana_react/public'; import type { DataVisualizerStartDependencies } from '../plugin'; +import type { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; -export type StartServices = CoreStart & DataVisualizerStartDependencies; +export type StartServices = CoreStart & + DataVisualizerStartDependencies & { + storage: IStorageWrapper; + }; export type DataVisualizerKibanaReactContextValue = KibanaReactContextValue; export const useDataVisualizerKibana = () => useKibana(); diff --git a/x-pack/plugins/file_upload/server/get_time_field_range.ts b/x-pack/plugins/file_upload/server/get_time_field_range.ts index 126269e22dd3a..84fc6ac002008 100644 --- a/x-pack/plugins/file_upload/server/get_time_field_range.ts +++ b/x-pack/plugins/file_upload/server/get_time_field_range.ts @@ -6,13 +6,14 @@ */ import { IScopedClusterClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { isPopulatedObject } from '../common/utils'; export async function getTimeFieldRange( client: IScopedClusterClient, index: string[] | string, timeFieldName: string, - query: any, + query: QueryDslQueryContainer, runtimeMappings?: estypes.MappingRuntimeFields ): Promise<{ success: boolean; diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 7a0601f9673cc..23804a8a6d618 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -24,12 +24,8 @@ import { I18nProvider } from '@kbn/i18n-react'; import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public'; import { checkForDuplicateTitle } from '../persistence'; import { createMemoryHistory } from 'history'; -import { - esFilters, - FilterManager, - IndexPattern, - Query, -} from '../../../../../src/plugins/data/public'; +import { FilterManager, IndexPattern, Query } from '../../../../../src/plugins/data/public'; +import { buildExistsFilter, FilterStateStore } from '@kbn/es-query'; import type { FieldSpec } from '../../../../../src/plugins/data/common'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { LensByValueInput } from '../embeddable/embeddable'; @@ -147,7 +143,7 @@ describe('Lens App', () => { const services = makeDefaultServicesForApp(); const indexPattern = { id: 'index1' } as unknown as IndexPattern; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); + const pinnedFilter = buildExistsFilter(pinnedField, indexPattern); services.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { return []; }); @@ -668,10 +664,10 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); - const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); + const unpinned = buildExistsFilter(field, indexPattern); + const pinned = buildExistsFilter(pinnedField, indexPattern); await act(async () => { - FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); }); const { services } = await save({ initialSavedObjectId: defaultSavedObjectId, @@ -882,14 +878,12 @@ describe('Lens App', () => { }), }); act(() => - services.data.query.filterManager.setFilters([ - esFilters.buildExistsFilter(field, indexPattern), - ]) + services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)]) ); instance.update(); expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ - filters: [esFilters.buildExistsFilter(field, indexPattern)], + filters: [buildExistsFilter(field, indexPattern)], }), }); }); @@ -932,9 +926,7 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; act(() => - services.data.query.filterManager.setFilters([ - esFilters.buildExistsFilter(field, indexPattern), - ]) + services.data.query.filterManager.setFilters([buildExistsFilter(field, indexPattern)]) ); instance.update(); expect(lensStore.getState()).toEqual({ @@ -1067,9 +1059,9 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); - const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); - FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + const unpinned = buildExistsFilter(field, indexPattern); + const pinned = buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); instance.update(); act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); @@ -1124,9 +1116,9 @@ describe('Lens App', () => { const indexPattern = { id: 'index1' } as unknown as IndexPattern; const field = { name: 'myfield' } as unknown as FieldSpec; const pinnedField = { name: 'pinnedField' } as unknown as FieldSpec; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); - const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); - FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + const unpinned = buildExistsFilter(field, indexPattern); + const pinned = buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); instance.update(); act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index ae144cdeb1068..e3098904a4b85 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; +import { isFilterPinned } from '@kbn/es-query'; import type { SavedObjectReference } from 'kibana/public'; import { SaveModal } from './save_modal'; @@ -15,7 +16,7 @@ import type { LensAppProps, LensAppServices } from './types'; import type { SaveProps } from './app'; import { Document, checkForDuplicateTitle } from '../persistence'; import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; -import { esFilters, FilterManager } from '../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../src/plugins/data/public'; import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common'; import { trackUiEvent } from '../lens_ui_telemetry'; import type { LensAppState } from '../state_management'; @@ -377,7 +378,7 @@ export function removePinnedFilters(doc?: Document) { ...doc, state: { ...doc.state, - filters: (doc.state?.filters || []).filter((filter) => !esFilters.isFilterPinned(filter)), + filters: (doc.state?.filters || []).filter((filter) => !isFilterPinned(filter)), }, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index fef71e92c5f2c..5d475be7bb83f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -27,8 +27,9 @@ import { WorkspacePanel } from './workspace_panel'; import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { fromExpression } from '@kbn/interpreter'; +import { buildExistsFilter } from '@kbn/es-query'; import { coreMock } from 'src/core/public/mocks'; -import { esFilters, IndexPattern } from '../../../../../../../src/plugins/data/public'; +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; import type { FieldSpec } from '../../../../../../../src/plugins/data/common'; import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; @@ -415,7 +416,7 @@ describe('workspace_panel', () => { instance.setProps({ framePublicAPI: { ...framePublicAPI, - filters: [esFilters.buildExistsFilter(field, indexPattern)], + filters: [buildExistsFilter(field, indexPattern)], }, }); }); diff --git a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts index 865e21f5bb613..e740c789a4874 100644 --- a/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts +++ b/x-pack/plugins/lens/public/mocks/data_plugin_mock.ts @@ -7,7 +7,8 @@ import { Observable, Subject } from 'rxjs'; import moment from 'moment'; -import { DataPublicPluginStart, esFilters, Filter } from '../../../../../src/plugins/data/public'; +import { isFilterPinned, Filter } from '@kbn/es-query'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; function createMockTimefilter() { const unsubscribe = jest.fn(); @@ -84,7 +85,7 @@ export function mockDataPlugin( getFilters: () => filters, getGlobalFilters: () => { // @ts-ignore - return filters.filter(esFilters.isFilterPinned); + return filters.filter(isFilterPinned); }, removeAll: () => { filters = []; diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index 31f0d67969b5d..f3b8595efc8d1 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -18,8 +18,14 @@ export { SOURCE_TYPES, STYLE_TYPE, SYMBOLIZE_AS_TYPES, + LAYER_WIZARD_CATEGORY, + MAX_ZOOM, + MIN_ZOOM, + VECTOR_SHAPE_TYPE, } from './constants'; +export type { FieldFormatter } from './constants'; + export type { EMSFileSourceDescriptor, ESTermSourceDescriptor, diff --git a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts index e0e12b7d51a56..47482cd4c4f92 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts @@ -23,8 +23,13 @@ export class TopTermPercentageField implements IESAggField { } supportsFieldMetaFromLocalData(): boolean { - // Elasticsearch vector tile search API does not support top term metric - return false; + if (this.getSource().isMvt()) { + // Elasticsearch vector tile search API does not support top term metric so meta tile does not contain any values + return false; + } else { + // field meta can be extracted from local data when field is geojson source + return true; + } } getSource(): IVectorSource { diff --git a/x-pack/plugins/maps/public/classes/layers/index.ts b/x-pack/plugins/maps/public/classes/layers/index.ts index 688cbd409c450..b068d4d234170 100644 --- a/x-pack/plugins/maps/public/classes/layers/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/index.ts @@ -6,4 +6,4 @@ */ export type { LayerWizard, LayerWizardWithMeta, RenderWizardArguments } from './wizards'; -export { getLayerWizards, registerLayerWizard } from './wizards'; +export { getLayerWizards, registerLayerWizardExternal } from './wizards'; diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx index 2670a6ac82c78..4334a34785433 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/choropleth_layer_wizard.tsx @@ -13,6 +13,7 @@ import { LayerTemplate } from './layer_template'; import { ChoroplethLayerIcon } from '../icons/cloropleth_layer_icon'; export const choroplethLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.choropleth.desc', { defaultMessage: 'Shaded areas to compare statistics across boundaries', diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx index 586667b9a1ecb..9be1639f7cb39 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/file_upload_wizard/config.tsx @@ -12,6 +12,7 @@ import { ClientFileCreateSourceEditor, UPLOAD_STEPS } from './wizard'; import { getFileUpload } from '../../../../kibana_services'; export const uploadLayerWizardConfig: LayerWizard = { + order: 10, categories: [], description: i18n.translate('xpack.maps.fileUploadWizard.description', { defaultMessage: 'Index GeoJSON data in Elasticsearch', diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/index.ts b/x-pack/plugins/maps/public/classes/layers/wizards/index.ts index d925fb50eb60d..814a2ec8e5c2f 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/index.ts @@ -10,4 +10,4 @@ export type { LayerWizardWithMeta, RenderWizardArguments, } from './layer_wizard_registry'; -export { getLayerWizards, registerLayerWizard } from './layer_wizard_registry'; +export { getLayerWizards, registerLayerWizardExternal } from './layer_wizard_registry'; diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.test.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.test.tsx new file mode 100644 index 0000000000000..aa54ea4286f1a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + getLayerWizards, + registerLayerWizardInternal, + registerLayerWizardExternal, +} from './layer_wizard_registry'; +import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; + +describe('LayerWizardRegistryTest', () => { + it('should enforce ordering', async () => { + registerLayerWizardExternal({ + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'foo', + renderWizard(): React.ReactElement { + return <>; + }, + order: 100, + }); + + registerLayerWizardInternal({ + order: 1, + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'foobar', + renderWizard(): React.ReactElement { + return <>; + }, + }); + + registerLayerWizardInternal({ + order: 1, + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'bar', + renderWizard(): React.ReactElement { + return <>; + }, + }); + + const wizards = await getLayerWizards(); + + expect(wizards[0].title).toBe('foobar'); + expect(wizards[1].title).toBe('bar'); + expect(wizards[2].title).toBe('foo'); + }); + + it('external users must add order higher than 99 ', async () => { + expect(() => { + registerLayerWizardExternal({ + order: 99, + categories: [LAYER_WIZARD_CATEGORY.REFERENCE], + description: '', + icon: '', + title: 'bar', + renderWizard(): React.ReactElement { + return <>; + }, + }); + }).toThrow(`layerWizard.order should be greater than or equal to '100'`); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts index 251af230d5278..6ab8a3d9a2f56 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/layer_wizard_registry.ts @@ -11,6 +11,26 @@ import { ReactElement, FunctionComponent } from 'react'; import type { LayerDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +export type LayerWizard = { + title: string; + categories: LAYER_WIZARD_CATEGORY[]; + /* + * Sets display order. + * Lower numbers are displayed before higher numbers. + * 0-99 reserved for Maps-plugin wizards. + */ + order: number; + description: string; + icon: string | FunctionComponent; + renderWizard(renderWizardArguments: RenderWizardArguments): ReactElement; + prerequisiteSteps?: Array<{ id: string; label: string }>; + disabledReason?: string; + getIsDisabled?: () => Promise | boolean; + isBeta?: boolean; + checkVisibility?: () => Promise; + showFeatureEditTools?: boolean; +}; + export type RenderWizardArguments = { previewLayers: (layerDescriptors: LayerDescriptor[]) => void; mapColors: string[]; @@ -27,20 +47,6 @@ export type RenderWizardArguments = { advanceToNextStep: () => void; }; -export type LayerWizard = { - categories: LAYER_WIZARD_CATEGORY[]; - checkVisibility?: () => Promise; - description: string; - disabledReason?: string; - getIsDisabled?: () => Promise | boolean; - isBeta?: boolean; - icon: string | FunctionComponent; - prerequisiteSteps?: Array<{ id: string; label: string }>; - renderWizard(renderWizardArguments: RenderWizardArguments): ReactElement; - title: string; - showFeatureEditTools?: boolean; -}; - export type LayerWizardWithMeta = LayerWizard & { isVisible: boolean; isDisabled: boolean; @@ -48,7 +54,7 @@ export type LayerWizardWithMeta = LayerWizard & { const registry: LayerWizard[] = []; -export function registerLayerWizard(layerWizard: LayerWizard) { +export function registerLayerWizardInternal(layerWizard: LayerWizard) { registry.push({ checkVisibility: async () => { return true; @@ -61,6 +67,13 @@ export function registerLayerWizard(layerWizard: LayerWizard) { }); } +export function registerLayerWizardExternal(layerWizard: LayerWizard) { + if (layerWizard.order < 100) { + throw new Error(`layerWizard.order should be greater than or equal to '100'`); + } + registerLayerWizardInternal(layerWizard); +} + export async function getLayerWizards(): Promise { const promises = registry.map(async (layerWizard: LayerWizard) => { return { @@ -69,7 +82,11 @@ export async function getLayerWizards(): Promise { isDisabled: await layerWizard.getIsDisabled!(), }; }); - return (await Promise.all(promises)).filter(({ isVisible }) => { - return isVisible; - }); + return (await Promise.all(promises)) + .filter(({ isVisible }) => { + return isVisible; + }) + .sort((wizard1: LayerWizardWithMeta, wizard2: LayerWizardWithMeta) => { + return wizard1.order - wizard2.order; + }); } diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts index 0b9558a393e78..3bf64d08fc845 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/load_layer_wizards.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { registerLayerWizard } from './layer_wizard_registry'; +import { registerLayerWizardInternal } from './layer_wizard_registry'; import { uploadLayerWizardConfig } from './file_upload_wizard'; import { esDocumentsLayerWizardConfig, @@ -16,17 +16,12 @@ import { heatmapLayerWizardConfig, } from '../../sources/es_geo_grid_source'; import { geoLineLayerWizardConfig } from '../../sources/es_geo_line_source'; -// @ts-ignore -import { point2PointLayerWizardConfig } from '../../sources/es_pew_pew_source'; -// @ts-ignore +import { point2PointLayerWizardConfig } from '../../sources/es_pew_pew_source/point_2_point_layer_wizard'; import { emsBoundariesLayerWizardConfig } from '../../sources/ems_file_source'; -// @ts-ignore import { emsBaseMapLayerWizardConfig } from '../../sources/ems_tms_source'; -// @ts-ignore -import { kibanaBasemapLayerWizardConfig } from '../../sources/kibana_tilemap_source'; +import { kibanaBasemapLayerWizardConfig } from '../../sources/kibana_tilemap_source/kibana_base_map_layer_wizard'; import { tmsLayerWizardConfig } from '../../sources/xyz_tms_source'; -// @ts-ignore -import { wmsLayerWizardConfig } from '../../sources/wms_source'; +import { wmsLayerWizardConfig } from '../../sources/wms_source/wms_layer_wizard'; import { mvtVectorSourceWizardConfig } from '../../sources/mvt_single_layer_vector_source'; import { ObservabilityLayerWizardConfig } from './solution_layers/observability'; import { SecurityLayerWizardConfig } from './solution_layers/security'; @@ -39,31 +34,23 @@ export function registerLayerWizards() { return; } - // Registration order determines display order - registerLayerWizard(uploadLayerWizardConfig); - registerLayerWizard(esDocumentsLayerWizardConfig); - // @ts-ignore - registerLayerWizard(choroplethLayerWizardConfig); - registerLayerWizard(clustersLayerWizardConfig); - // @ts-ignore - registerLayerWizard(heatmapLayerWizardConfig); - registerLayerWizard(esTopHitsLayerWizardConfig); - registerLayerWizard(geoLineLayerWizardConfig); - // @ts-ignore - registerLayerWizard(point2PointLayerWizardConfig); - // @ts-ignore - registerLayerWizard(emsBoundariesLayerWizardConfig); - registerLayerWizard(newVectorLayerWizardConfig); - // @ts-ignore - registerLayerWizard(emsBaseMapLayerWizardConfig); - // @ts-ignore - registerLayerWizard(kibanaBasemapLayerWizardConfig); - registerLayerWizard(tmsLayerWizardConfig); - // @ts-ignore - registerLayerWizard(wmsLayerWizardConfig); + registerLayerWizardInternal(uploadLayerWizardConfig); + registerLayerWizardInternal(esDocumentsLayerWizardConfig); + registerLayerWizardInternal(choroplethLayerWizardConfig); + registerLayerWizardInternal(clustersLayerWizardConfig); + registerLayerWizardInternal(heatmapLayerWizardConfig); + registerLayerWizardInternal(esTopHitsLayerWizardConfig); + registerLayerWizardInternal(geoLineLayerWizardConfig); + registerLayerWizardInternal(point2PointLayerWizardConfig); + registerLayerWizardInternal(emsBoundariesLayerWizardConfig); + registerLayerWizardInternal(newVectorLayerWizardConfig); + registerLayerWizardInternal(emsBaseMapLayerWizardConfig); + registerLayerWizardInternal(kibanaBasemapLayerWizardConfig); + registerLayerWizardInternal(tmsLayerWizardConfig); + registerLayerWizardInternal(wmsLayerWizardConfig); - registerLayerWizard(mvtVectorSourceWizardConfig); - registerLayerWizard(ObservabilityLayerWizardConfig); - registerLayerWizard(SecurityLayerWizardConfig); + registerLayerWizardInternal(mvtVectorSourceWizardConfig); + registerLayerWizardInternal(ObservabilityLayerWizardConfig); + registerLayerWizardInternal(SecurityLayerWizardConfig); registered = true; } diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx index d6376470ae71f..85fae39a05910 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/new_vector_layer_wizard/config.tsx @@ -16,6 +16,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../../common/constants'; const ADD_VECTOR_DRAWING_LAYER = 'ADD_VECTOR_DRAWING_LAYER'; export const newVectorLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.newVectorLayerWizard.description', { defaultMessage: 'Draw shapes on the map and index in Elasticsearch', diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx index a69b09fffd9ee..2e023f7c588d3 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/observability_layer_wizard.tsx @@ -14,6 +14,7 @@ import { APM_INDEX_PATTERN_ID } from './create_layer_descriptor'; import { getIndexPatternService } from '../../../../../kibana_services'; export const ObservabilityLayerWizardConfig: LayerWizard = { + order: 20, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH, LAYER_WIZARD_CATEGORY.SOLUTIONS], getIsDisabled: async () => { try { diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx index f055683722deb..79575ea815124 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/security_layer_wizard.tsx @@ -13,6 +13,7 @@ import { getSecurityIndexPatterns } from './security_index_pattern_utils'; import { SecurityLayerTemplate } from './security_layer_template'; export const SecurityLayerWizardConfig: LayerWizard = { + order: 20, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH, LAYER_WIZARD_CATEGORY.SOLUTIONS], getIsDisabled: async () => { const indexPatterns = await getSecurityIndexPatterns(); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index ff899101ced49..8fe8f1b3a155f 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -29,6 +29,7 @@ function getDescription() { } export const emsBoundariesLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { const emsSettings = getEMSSettings(); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 26afa65b9527c..27d911cc8feb9 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -27,6 +27,7 @@ function getDescription() { } export const emsBaseMapLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { const emsSettings = getEMSSettings(); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index a184ae7b7ce56..e075a615d5867 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -33,6 +33,7 @@ import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; import { ClustersLayerIcon } from '../../layers/wizards/icons/clusters_layer_icon'; export const clustersLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGridClustersDescription', { defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index b415b7a167c5a..5e67a83811561 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -17,6 +17,7 @@ import { GRID_RESOLUTION, LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../c import { HeatmapLayerIcon } from '../../layers/wizards/icons/heatmap_layer_icon'; export const heatmapLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', { defaultMessage: 'Geospatial data grouped in grids to show density', diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx index 85932658383de..18d459ddbcb78 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx @@ -17,6 +17,7 @@ import { getIsGoldPlus } from '../../../licensed_features'; import { TracksLayerIcon } from '../../layers/wizards/icons/tracks_layer_icon'; export const geoLineLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esGeoLineDescription', { defaultMessage: 'Create lines from points', diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index c35b8677c1093..e3522d39e892d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -27,6 +27,7 @@ import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/desc import { Point2PointLayerIcon } from '../../layers/wizards/icons/point_2_point_layer_icon'; export const point2PointLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.pewPewDescription', { defaultMessage: 'Aggregated data paths between the source and destination', diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index b5aeb28715aef..82fb1c502ef6a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -35,6 +35,7 @@ export function createDefaultLayerDescriptor( } export const esDocumentsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.esSearchDescription', { defaultMessage: 'Points, lines, and polygons from Elasticsearch', diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx index 3e5af637dc336..7c01fed158b0d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx @@ -16,6 +16,7 @@ import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types import { ESSearchSource } from '../es_search_source'; export const esTopHitsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], description: i18n.translate('xpack.maps.source.topHitsDescription', { defaultMessage: diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index ec69989a8313d..0f3475eeae9ee 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -17,6 +17,7 @@ import { getKibanaTileMap } from '../../../util'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], checkVisibility: async () => { const tilemap = getKibanaTileMap(); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index bf6ff368594cc..f123ed7c78054 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -16,6 +16,7 @@ import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descrip import { VectorTileLayerIcon } from '../../layers/wizards/icons/vector_tile_layer_icon'; export const mvtVectorSourceWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { defaultMessage: 'Data service implementing the Mapbox vector tile specification', diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index 19f31d481f58e..2f79b8d0984d0 100644 --- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -17,6 +17,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WebMapServiceLayerIcon } from '../../layers/wizards/icons/web_map_service_layer_icon'; export const wmsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.wmsDescription', { defaultMessage: 'Maps from OGC Standard WMS', diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index 82aab592a1344..7c137419f4a19 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -15,6 +15,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WorldMapLayerIcon } from '../../layers/wizards/icons/world_map_layer_icon'; export const tmsLayerWizardConfig: LayerWizard = { + order: 10, categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.ems_xyzDescription', { defaultMessage: 'Raster image tile map service using {z}/{x}/{y} url pattern.', diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts index a264ae36d88af..6b0e212357436 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts @@ -31,18 +31,19 @@ export const DEFAULT_ICON_SIZE = 6; export const DEFAULT_COLOR_RAMP = NUMERICAL_COLOR_PALETTES[0].value; export const DEFAULT_COLOR_PALETTE = CATEGORICAL_COLOR_PALETTES[0].value; -export const LINE_STYLES = [VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH]; -export const POLYGON_STYLES = [ - VECTOR_STYLES.FILL_COLOR, - VECTOR_STYLES.LINE_COLOR, - VECTOR_STYLES.LINE_WIDTH, -]; export const LABEL_STYLES = [ VECTOR_STYLES.LABEL_SIZE, VECTOR_STYLES.LABEL_COLOR, VECTOR_STYLES.LABEL_BORDER_COLOR, VECTOR_STYLES.LABEL_BORDER_SIZE, ]; +export const LINE_STYLES = [VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH, ...LABEL_STYLES]; +export const POLYGON_STYLES = [ + VECTOR_STYLES.FILL_COLOR, + VECTOR_STYLES.LINE_COLOR, + VECTOR_STYLES.LINE_WIDTH, + ...LABEL_STYLES, +]; export function getDefaultStaticProperties( mapColors: string[] = [] diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 40ee17d176706..a3b6638d5ee8b 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -15,6 +15,7 @@ import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { EuiEmptyPrompt } from '@elastic/eui'; import { Filter } from '@kbn/es-query'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { Embeddable, IContainer, @@ -72,6 +73,7 @@ import { getChartsPaletteServiceGetColor, getSpacesApi, getSearchService, + getTheme, } from '../kibana_services'; import { LayerDescriptor, MapExtent } from '../../common/descriptor_types'; import { MapContainer } from '../connected_components/map_container'; @@ -400,7 +402,9 @@ export class MapEmbeddable const I18nContext = getCoreI18n().Context; render( - {content} + + {content}; + , this._domNode ); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 31066204cd318..b11d7270fe13e 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -46,7 +46,7 @@ import { MapsStartApi, suggestEMSTermJoinConfig, } from './api'; -import { registerLayerWizard } from './classes/layers'; +import { registerLayerWizardExternal } from './classes/layers'; import { registerSource } from './classes/sources/source_registry'; import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import type { MapsEmsPluginPublicStart } from '../../../../src/plugins/maps_ems/public'; @@ -182,7 +182,7 @@ export class MapsPlugin plugins.visualizations.createBaseVisualization(tileMapVisType); return { - registerLayerWizard, + registerLayerWizard: registerLayerWizardExternal, registerSource, }; } diff --git a/x-pack/plugins/ml/common/types/storage.ts b/x-pack/plugins/ml/common/types/storage.ts index 6da8076e22332..22374a5533fab 100644 --- a/x-pack/plugins/ml/common/types/storage.ts +++ b/x-pack/plugins/ml/common/types/storage.ts @@ -13,6 +13,8 @@ export const ML_APPLY_TIME_RANGE_CONFIG = 'ml.jobSelectorFlyout.applyTimeRange'; export const ML_GETTING_STARTED_CALLOUT_DISMISSED = 'ml.gettingStarted.isDismissed'; +export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; + export type PartitionFieldConfig = | { /** @@ -44,6 +46,7 @@ export type MlStorage = Partial<{ [ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig; [ML_APPLY_TIME_RANGE_CONFIG]: ApplyTimeRangeConfig; [ML_GETTING_STARTED_CALLOUT_DISMISSED]: boolean | undefined; + [ML_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen'; }> | null; export type MlStorageKey = keyof Exclude; diff --git a/x-pack/plugins/ml/common/util/query_utils.test.ts b/x-pack/plugins/ml/common/util/query_utils.test.ts new file mode 100644 index 0000000000000..947b87e9976d5 --- /dev/null +++ b/x-pack/plugins/ml/common/util/query_utils.test.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 { addExcludeFrozenToQuery } from './query_utils'; + +describe('Util: addExcludeFrozenToQuery()', () => { + test('Validation checks.', () => { + expect( + addExcludeFrozenToQuery({ + match_all: {}, + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }) + ).toMatchObject({ + bool: { + must: [{ match_all: {} }], + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: { + term: { + category: { + value: 'clothing', + }, + }, + }, + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: [{ term: { category: { value: 'clothing' } } }], + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect(addExcludeFrozenToQuery(undefined)).toMatchObject({ + bool: { + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/query_utils.ts b/x-pack/plugins/ml/common/util/query_utils.ts new file mode 100644 index 0000000000000..22c0f45f2f239 --- /dev/null +++ b/x-pack/plugins/ml/common/util/query_utils.ts @@ -0,0 +1,53 @@ +/* + * Copyright 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { cloneDeep } from 'lodash'; +import { isPopulatedObject } from './object_utils'; + +export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { + const FROZEN_TIER_TERM = { + term: { + _tier: { + value: 'data_frozen', + }, + }, + }; + + if (!originalQuery) { + return { + bool: { + must_not: [FROZEN_TIER_TERM], + }, + }; + } + + const query = cloneDeep(originalQuery); + + delete query.match_all; + + if (isPopulatedObject(query.bool)) { + // Must_not can be both arrays or singular object + if (Array.isArray(query.bool.must_not)) { + query.bool.must_not.push(FROZEN_TIER_TERM); + } else { + // If there's already a must_not condition + if (isPopulatedObject(query.bool.must_not)) { + query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM]; + } + if (query.bool.must_not === undefined) { + query.bool.must_not = [FROZEN_TIER_TERM]; + } + } + } else { + query.bool = { + must_not: [FROZEN_TIER_TERM], + }; + } + + return query; +}; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap b/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap index eb9705f3438aa..9a3fb9b29d09b 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap @@ -1,19 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FullTimeRangeSelector renders the selector 1`] = ` - - } - /> - + delay="regular" + display="inlineBlock" + position="top" + > + + + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mlFullTimeRangeSelectorOption" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + `; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx index d04f8f7b648f5..3f64ff794d9ab 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx @@ -20,6 +20,12 @@ jest.mock('./full_time_range_selector_service', () => ({ mockSetFullTimeRange(indexPattern, query), })); +jest.mock('../../contexts/ml/use_storage', () => { + return { + useStorage: jest.fn(() => 'exclude-frozen'), + }; +}); + describe('FullTimeRangeSelector', () => { const dataView = { id: '0844fc70-5ab5-11e9-935e-836737467b0f', diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx index f0af666e07dbc..44f6fc5e604cb 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx @@ -5,44 +5,160 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { Query } from 'src/plugins/data/public'; -import { EuiButton } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiButton, + EuiFlexItem, + EuiButtonIcon, + EuiRadioGroup, + EuiPanel, + EuiToolTip, + EuiPopover, + EuiRadioGroupOption, +} from '@elastic/eui'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { i18n } from '@kbn/i18n'; import type { DataView } from '../../../../../../../src/plugins/data_views/public'; import { setFullTimeRange } from './full_time_range_selector_service'; +import { useStorage } from '../../contexts/ml/use_storage'; +import { ML_FROZEN_TIER_PREFERENCE } from '../../../../common/types/storage'; interface Props { dataView: DataView; - query: Query; + query: QueryDslQueryContainer; disabled: boolean; callback?: (a: any) => void; } +const FROZEN_TIER_PREFERENCE = { + EXCLUDE: 'exclude-frozen', + INCLUDE: 'include-frozen', +} as const; + +type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE]; + // Component for rendering a button which automatically sets the range of the time filter // to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query. export const FullTimeRangeSelector: FC = ({ dataView, query, disabled, callback }) => { // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop - async function setRange(i: DataView, q: Query) { - const fullTimeRange = await setFullTimeRange(i, q); + async function setRange(i: DataView, q: QueryDslQueryContainer, excludeFrozenData = true) { + const fullTimeRange = await setFullTimeRange(i, q, excludeFrozenData); if (typeof callback === 'function') { callback(fullTimeRange); } } + + const [isPopoverOpen, setPopover] = useState(false); + const [frozenDataPreference, setFrozenDataPreference] = useStorage( + ML_FROZEN_TIER_PREFERENCE, + FROZEN_TIER_PREFERENCE.EXCLUDE + ); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const sortOptions: EuiRadioGroupOption[] = useMemo(() => { + return [ + { + id: FROZEN_TIER_PREFERENCE.EXCLUDE, + label: i18n.translate( + 'xpack.ml.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel', + { + defaultMessage: 'Exclude frozen data tier', + } + ), + }, + { + id: FROZEN_TIER_PREFERENCE.INCLUDE, + label: i18n.translate( + 'xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel', + { + defaultMessage: 'Include frozen data tier', + } + ), + }, + ]; + }, []); + + const setPreference = useCallback((id: string) => { + setFrozenDataPreference(id as FrozenTierPreference); + setRange(dataView, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE); + closePopover(); + }, []); + + const popoverContent = useMemo( + () => ( + + + + ), + [frozenDataPreference, sortOptions] + ); + + const buttonTooltip = useMemo( + () => + frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( + + ) : ( + + ), + [frozenDataPreference] + ); + return ( - setRange(dataView, query)} - data-test-subj="mlButtonUseFullData" - > - - + + + setRange(dataView, query, true)} + data-test-subj="mlButtonUseFullData" + > + + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downRight" + > + {popoverContent} + + + ); }; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts index 8f0d344a36f36..7e14639f1b8b4 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts @@ -8,13 +8,14 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import type { Query } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { getTimefilter, getToastNotifications } from '../../util/dependency_cache'; import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service'; import type { DataView } from '../../../../../../../src/plugins/data_views/public'; import { isPopulatedObject } from '../../../../common/util/object_utils'; -import { RuntimeMappings } from '../../../../common/types/fields'; +import type { RuntimeMappings } from '../../../../common/types/fields'; +import { addExcludeFrozenToQuery } from '../../../../common/util/query_utils'; export interface TimeRange { from: number; @@ -23,7 +24,8 @@ export interface TimeRange { export async function setFullTimeRange( indexPattern: DataView, - query: Query + query: QueryDslQueryContainer, + excludeFrozenData: boolean ): Promise { try { const timefilter = getTimefilter(); @@ -31,7 +33,8 @@ export async function setFullTimeRange( const resp = await ml.getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, - query, + // By default we want to use full non-frozen time range + query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query, ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); timefilter.setTime({ diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index a6a707634811d..c370778b178c8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -42,6 +42,7 @@ import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; import { isPopulatedObject } from '../../../../../common/util/object_utils'; import { RuntimeMappings } from '../../../../../common/types/fields'; +import { addExcludeFrozenToQuery } from '../../../../../common/util/query_utils'; import { MlPageHeader } from '../../../components/page_header'; export interface ModuleJobUI extends ModuleJob { @@ -136,7 +137,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const { start, end } = await ml.getTimeFieldRange({ index: dataView.title, timeFieldName: dataView.timeFieldName, - query: combinedQuery, + // By default we want to use full non-frozen time range + query: addExcludeFrozenToQuery(combinedQuery), ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); return { diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index e608bfeb622d8..128517777bb46 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -16,7 +16,6 @@ import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs'; import { RuntimeMappings } from '../../../common/types/fields'; import { isPopulatedObject } from '../../../common/util/object_utils'; - /** * Service for carrying out queries to obtain data * specific to fields in Elasticsearch indices. diff --git a/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap b/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap index 13fbd618a0a09..f52e00b4432c0 100644 --- a/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/sparkline/__snapshots__/index.test.js.snap @@ -18,7 +18,7 @@ exports[`Sparkline component shows tooltip on hover 1`] = ` style={ Object { "left": 210, - "top": 27, + "top": 17, } } > @@ -35,7 +35,7 @@ exports[`Sparkline component shows tooltip on hover 1`] = ` className="monSparklineTooltip" style={ Object { - "height": 36, + "height": 56, "width": 220, } } diff --git a/x-pack/plugins/monitoring/public/components/sparkline/index.js b/x-pack/plugins/monitoring/public/components/sparkline/index.js index fe399545cf6e0..ee250624432a6 100644 --- a/x-pack/plugins/monitoring/public/components/sparkline/index.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/index.js @@ -60,7 +60,7 @@ export class Sparkline extends React.Component { return; } - const tooltipHeightInPx = 36; + const tooltipHeightInPx = 56; const tooltipWidthInPx = 220; const caretWidthInPx = 6; const marginBetweenPointAndCaretInPx = 10; diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index fcc3fdd64c36c..03860fd3cd122 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -24,3 +24,7 @@ export const observabilityFeatureId = 'observability'; // Used by Cases to install routes export const casesPath = '/cases'; + +// Name of a locator created by the uptime plugin. Intended for use +// by other plugins as well, so defined here to prevent cross-references. +export const uptimeOverviewLocatorID = 'uptime-overview-locator'; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 9a45dbcbdbd64..e502cf7fb37e0 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -24,6 +24,7 @@ export type { ObservabilityPublicPluginsStart, }; export { enableInspectEsQueries } from '../common/ui_settings_keys'; +export { uptimeOverviewLocatorID } from '../common'; export interface ConfigSchema { unsafe: { diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts index 4de1160e53936..c3e589447313a 100644 --- a/x-pack/plugins/security_solution/common/ecs/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -74,4 +74,5 @@ export interface Ecs { Target?: Target; dll?: DllEcs; 'kibana.alert.workflow_status'?: 'open' | 'acknowledged' | 'in-progress' | 'closed'; + 'kibana.alert.rule.parameters'?: { index: string[] }; } diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index e1208c7c54a3b..d764a12f951d8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -165,6 +165,8 @@ describe('Detection rules, EQL', () => { .invoke('text') .then((text) => { expect(text).contains(this.rule.name); + expect(text).contains(this.rule.severity.toLowerCase()); + expect(text).contains(this.rule.riskScore); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts index 7eedc99652f80..829f98b4a537d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts @@ -29,6 +29,7 @@ import { LOADING_SPINNER, EXCEPTION_ITEM_CONTAINER, ADD_EXCEPTIONS_BTN, + EXCEPTION_FIELD_LIST, } from '../../screens/exceptions'; import { ALERTS_URL } from '../../urls/navigation'; @@ -196,4 +197,13 @@ describe('Exceptions modal', () => { closeExceptionBuilderModal(); }); + + it('Contains custom index fields', () => { + cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); + + cy.get(FIELD_INPUT).eq(0).click({ force: true }); + cy.get(EXCEPTION_FIELD_LIST).contains('unique_value.test'); + + closeExceptionBuilderModal(); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index e5027ee8b4f3a..9bba5e4b555dc 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -56,3 +56,6 @@ export const EXCEPTIONS_TABLE_MODAL = '[data-test-subj="referenceErrorModal"]'; export const EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN = '[data-test-subj="confirmModalConfirmButton"]'; export const EXCEPTION_ITEM_CONTAINER = '[data-test-subj="exceptionEntriesContainer"]'; + +export const EXCEPTION_FIELD_LIST = + '[data-test-subj="comboBoxOptionsList fieldAutocompleteComboBox-optionsList"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 1ba95cc2a2951..d64864a699a60 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -121,6 +121,9 @@ const AlertContextMenuComponent: React.FC !activityLastLogData?.data.length && !activityLogData.length, + [activityLastLogData, activityLogData] + ); + const showCallout = useMemo( - () => !isPagingDisabled && activityLogLoaded && !activityLogData.length, - [isPagingDisabled, activityLogLoaded, activityLogData] + () => + (!isPagingDisabled && activityLogLoaded && !activityLogData.length) || + doesNotHaveDataAlsoOnRefetch, + [isPagingDisabled, activityLogLoaded, activityLogData, doesNotHaveDataAlsoOnRefetch] ); const loadMoreTrigger = useRef(null); @@ -153,7 +160,7 @@ export const EndpointActivityLog = memo( ref={loadMoreTrigger} /> )} - {isPagingDisabled && !activityLogLoading && ( + {isPagingDisabled && !activityLogLoading && !showCallout && (

{i18.ACTIVITY_LOG.LogEntry.endOfLog}

diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 4732e28c7f828..ddd7349fadc51 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -1028,6 +1028,57 @@ describe('when on the endpoint list page', () => { expect(activityLogCallout).not.toBeNull(); }); + it('should display a callout message if no log data also on refetch', async () => { + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + history.push( + getEndpointDetailsPath({ + page_index: '0', + page_size: '10', + name: 'endpointActivityLog', + selected_endpoint: '1', + }) + ); + }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_endpoint=1&show=activity_log' + ); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [], + }); + }); + + const activityLogCallout = await renderResult.findByTestId('activityLogNoDataCallout'); + expect(activityLogCallout).not.toBeNull(); + + // click refresh button + const refreshLogButton = await renderResult.findByTestId('superDatePickerApplyTimeButton'); + userEvent.click(refreshLogButton); + + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [], + }); + }); + + const activityLogNoDataCallout = await renderResult.findByTestId( + 'activityLogNoDataCallout' + ); + expect(activityLogNoDataCallout).not.toBeNull(); + }); + it('should not display scroll trigger when showing callout message', async () => { const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx index 4d53682d2d669..ac944371acdda 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/empty/policy_event_filters_empty_unassigned.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback } from 'react'; import { EuiButton, EuiEmptyPrompt, EuiPageTemplate, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; +import { usePolicyDetailsEventFiltersNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_event_filters_empty_hooks'; import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; @@ -21,7 +21,7 @@ export const PolicyEventFiltersEmptyUnassigned = memo(({ policyId, const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); - const navigateCallback = usePolicyDetailsNavigateCallback(); + const navigateCallback = usePolicyDetailsEventFiltersNavigateCallback(); const onClickPrimaryButtonHandler = useCallback( () => navigateCallback({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx index 5b4138480fdf3..850a303654c52 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/event_filters/layout/policy_event_filters_layout.tsx @@ -114,10 +114,23 @@ export const PolicyEventFiltersLayout = React.memo - ) : ( - + return ( + <> + {canCreateArtifactsByPolicy && urlParams.show === 'list' && ( + + )} + {allEventFilters && allEventFilters.total !== 0 ? ( + + ) : ( + + )} + ); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index ac889ebc55cdf..50d553045b5d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -184,7 +184,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } } catch (exc) { const errorMessage = buildRuleMessage(`Check privileges failed to execute ${exc}`); - logger.error(errorMessage); + logger.warn(errorMessage); await ruleStatusClient.logStatusChange({ ...basicLogArguments, message: errorMessage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 13106ec3012be..6842cc5bda230 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -661,7 +661,7 @@ describe('utils', () => { }, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'myfakerulename', @@ -677,11 +677,12 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); }); + test('returns true when missing timestamp field', async () => { const timestampField = '@timestamp'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -706,7 +707,7 @@ describe('utils', () => { }, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'myfakerulename', @@ -722,7 +723,7 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); @@ -737,7 +738,7 @@ describe('utils', () => { fields: {}, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'Endpoint Security', @@ -753,7 +754,7 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); @@ -768,7 +769,7 @@ describe('utils', () => { fields: {}, }, }; - mockLogger.error.mockClear(); + mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, ruleName: 'NOT Endpoint Security', @@ -784,7 +785,7 @@ describe('utils', () => { logger: mockLogger, buildRuleMessage, }); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockLogger.warn).toHaveBeenCalledWith( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); expect(res).toBeTruthy(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 60df18847939b..4efe356525c1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -118,7 +118,7 @@ export const hasReadIndexPrivileges = async (args: { const errorString = `This rule may not have the required read privileges to the following indices/index patterns: ${JSON.stringify( indexesWithNoReadPrivileges )}`; - logger.error(buildRuleMessage(errorString)); + logger.warn(buildRuleMessage(errorString)); await ruleStatusClient.logStatusChange({ message: errorString, ruleId, @@ -168,7 +168,7 @@ export const hasTimestampFields = async (args: { ? 'If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent.' : '' }`; - logger.error(buildRuleMessage(errorString.trimEnd())); + logger.warn(buildRuleMessage(errorString.trimEnd())); await ruleStatusClient.logStatusChange({ message: errorString.trimEnd(), ruleId, @@ -195,7 +195,7 @@ export const hasTimestampFields = async (args: { ? timestampFieldCapsResponse.body.indices : timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices )}`; - logger.error(buildRuleMessage(errorString)); + logger.warn(buildRuleMessage(errorString)); await ruleStatusClient.logStatusChange({ message: errorString, ruleId, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 901fbbef6714b..36f16182b17e9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -235,7 +235,6 @@ "console.settingsPage.indicesAndAliasesLabelText": "インデックスとエイリアス", "console.settingsPage.jsonSyntaxLabel": "JSON構文", "console.settingsPage.pageTitle": "コンソール設定", - "console.settingsPage.pollingLabelText": "自動入力候補を自動的に更新", "console.settingsPage.refreshButtonLabel": "自動入力候補の更新", "console.settingsPage.refreshingDataDescription": "コンソールは、Elasticsearchをクエリして自動入力候補を更新します。クラスターが大きい場合や、ネットワークの制限がある場合には、自動更新で問題が発生する可能性があります。", "console.settingsPage.refreshingDataLabel": "自動入力候補を更新しています", @@ -6565,7 +6564,7 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "Elasticsearch インデックスを取得できませんでした", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "「{functionName}」のレンダリングが失敗しました", "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした", - "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", + "xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした", "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "ワークパッドが見つかりませんでした", "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした", @@ -8388,7 +8387,6 @@ "xpack.dataVisualizer.index.fieldNameSelect": "フィールド名", "xpack.dataVisualizer.index.fieldTypeSelect": "フィールド型", "xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "時間範囲の設定中にエラーが発生しました。", - "xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "完全な {indexPatternTitle} データを使用", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "異常検知は時間ベースのインデックスでのみ実行されます", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "インデックスパターン {indexPatternTitle} は時系列に基づくものではありません", "xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName}の平均", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 76eac37d763c9..4ec36c767a5d3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -238,7 +238,6 @@ "console.settingsPage.indicesAndAliasesLabelText": "索引和别名", "console.settingsPage.jsonSyntaxLabel": "JSON 语法", "console.settingsPage.pageTitle": "控制台设置", - "console.settingsPage.pollingLabelText": "自动刷新自动完成建议", "console.settingsPage.refreshButtonLabel": "刷新自动完成建议", "console.settingsPage.refreshingDataDescription": "控制台通过查询 Elasticsearch 来刷新自动完成建议。如果您的集群较大或您的网络有限制,则自动刷新可能会造成问题。", "console.settingsPage.refreshingDataLabel": "正在刷新自动完成建议", @@ -6610,7 +6609,7 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "无法提取 Elasticsearch 索引", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "呈现“{functionName}”失败。", "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "无法克隆 Workpad", - "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", + "xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "无法删除所有 Workpad", "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "无法查找 Workpad", "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件", @@ -8461,7 +8460,6 @@ "xpack.dataVisualizer.index.fieldNameSelect": "字段名称", "xpack.dataVisualizer.index.fieldTypeSelect": "字段类型", "xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "设置时间范围时出错。", - "xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "使用完整的 {indexPatternTitle} 数据", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "仅针对基于时间的索引运行异常检测", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "索引模式 {indexPatternTitle} 不基于时间序列", "xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName} 的平均值", diff --git a/x-pack/plugins/uptime/common/config.ts b/x-pack/plugins/uptime/common/config.ts index 38ba7b7b3fd48..08db653214208 100644 --- a/x-pack/plugins/uptime/common/config.ts +++ b/x-pack/plugins/uptime/common/config.ts @@ -7,46 +7,50 @@ import { PluginConfigDescriptor } from 'kibana/server'; import { schema, TypeOf } from '@kbn/config-schema'; +import { sslSchema } from '@kbn/server-http-tools'; -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - ui: true, - }, - schema: schema.maybe( +const serviceConfig = schema.object({ + enabled: schema.boolean(), + username: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + manifestUrl: schema.string(), + hosts: schema.maybe(schema.arrayOf(schema.string())), + syncInterval: schema.maybe(schema.string()), + tls: schema.maybe(sslSchema), +}); + +const uptimeConfig = schema.object({ + index: schema.maybe(schema.string()), + ui: schema.maybe( schema.object({ - index: schema.maybe(schema.string()), - ui: schema.maybe( - schema.object({ - unsafe: schema.maybe( - schema.object({ - monitorManagement: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - }) - ), - }) - ), unsafe: schema.maybe( schema.object({ - service: schema.maybe( + monitorManagement: schema.maybe( schema.object({ enabled: schema.boolean(), - username: schema.string(), - password: schema.string(), - manifestUrl: schema.string(), - hosts: schema.maybe(schema.arrayOf(schema.string())), - syncInterval: schema.maybe(schema.string()), }) ), }) ), }) ), + unsafe: schema.maybe( + schema.object({ + service: serviceConfig, + }) + ), +}); + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: uptimeConfig, }; -export type UptimeConfig = TypeOf; +export type UptimeConfig = TypeOf; +export type ServiceConfig = TypeOf; + export interface UptimeUiConfig { ui?: TypeOf['ui']; } diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts new file mode 100644 index 0000000000000..d8746d715581d --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/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 './tls_alert_flyouts_in_alerting_app'; +export * from './status_alert_flyouts_in_alerting_app'; diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts new file mode 100644 index 0000000000000..ba973a7aa8a61 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before } from '@elastic/synthetics'; +import { assertText, byTestId, loginToKibana, waitForLoadingToFinish } from '../utils'; + +journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const baseUrl = `${params.kibanaUrl}/app/management/insightsAndAlerting/triggersActions/rules`; + + step('Go to Alerting app', async () => { + await page.goto(`${baseUrl}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('Open monitor status flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.monitorStatus-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'This alert will apply to approximately 0 monitors.' }); + }); + + step('can add filters', async () => { + await page.click('text=Add filter'); + await page.click(byTestId('"uptimeAlertAddFilter.monitor.type"')); + await page.click(byTestId('"uptimeCreateStatusAlert.filter_scheme"')); + }); + + step('can open query bar', async () => { + await page.click(byTestId('"xpack.uptime.alerts.monitorStatus.filterBar"')); + + await page.fill(byTestId('"xpack.uptime.alerts.monitorStatus.filterBar"'), 'monitor.type : '); + + await waitForLoadingToFinish({ page }); + + await assertText({ page, text: 'browser' }); + await assertText({ page, text: 'http' }); + + const suggestionItem = await page.$(byTestId('autoCompleteSuggestionText')); + expect(await suggestionItem?.textContent()).toBe('"browser" '); + + await page.click(byTestId('euiFlyoutCloseButton')); + await page.click(byTestId('confirmModalConfirmButton')); + }); + + step('Open tls alert flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'has a certificate expiring within' }); + }); + + step('Tls alert flyout has setting values', async () => { + await assertText({ page, text: '30 days' }); + await assertText({ page, text: '730 days' }); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts new file mode 100644 index 0000000000000..024e8e53c3b2a --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.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 { journey, step, before } from '@elastic/synthetics'; +import { assertText, byTestId, loginToKibana, waitForLoadingToFinish } from '../utils'; + +journey('TlsFlyoutInAlertingApp', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const baseUrl = `${params.kibanaUrl}/app/management/insightsAndAlerting/triggersActions/rules`; + + step('Go to Alerting app', async () => { + await page.goto(`${baseUrl}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('Open tls alert flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'has a certificate expiring within' }); + }); + + step('Tls alert flyout has setting values', async () => { + await assertText({ page, text: '30 days' }); + await assertText({ page, text: '730 days' }); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index 89abed5ce8f29..6bdea1beb016b 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -7,3 +7,4 @@ export * from './uptime.journey'; export * from './step_duration.journey'; +export * from './alerts'; diff --git a/x-pack/plugins/uptime/e2e/journeys/utils.ts b/x-pack/plugins/uptime/e2e/journeys/utils.ts index 3188c86f82049..6d2f1dd554108 100644 --- a/x-pack/plugins/uptime/e2e/journeys/utils.ts +++ b/x-pack/plugins/uptime/e2e/journeys/utils.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { Page } from '@elastic/synthetics'; -import { byTestId } from './uptime.journey'; +import { expect, Page } from '@elastic/synthetics'; export async function waitForLoadingToFinish({ page }: { page: Page }) { while (true) { @@ -25,3 +24,12 @@ export async function loginToKibana({ page }: { page: Page }) { await waitForLoadingToFinish({ page }); } + +export const byTestId = (testId: string) => { + return `[data-test-subj=${testId}]`; +}; + +export const assertText = async ({ page, text }: { page: Page; text: string }) => { + await page.waitForSelector(`text=${text}`); + expect(await page.$(`text=${text}`)).toBeTruthy(); +}; diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index fe4d3ff804bf9..0581692e0e278 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -13,18 +13,22 @@ import { esArchiverLoad, esArchiverUnload } from './tasks/es_archiver'; import './journeys'; +const listOfJourneys = [ + 'uptime', + 'StepsDuration', + 'TlsFlyoutInAlertingApp', + 'StatusFlyoutInAlertingApp', +] as const; + export function playwrightRunTests({ headless, match }: { headless: boolean; match?: string }) { return async ({ getService }: any) => { const result = await playwrightStart(getService, headless, match); - if ( - result?.uptime && - result.uptime.status !== 'succeeded' && - result.StepsDuration && - result.StepsDuration.status !== 'succeeded' - ) { - throw new Error('Tests failed'); - } + listOfJourneys.forEach((journey) => { + if (result?.[journey] && result[journey].status !== 'succeeded') { + throw new Error('Tests failed'); + } + }); }; } diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index f3971b6bd4bf3..35be0b19d4521 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,43 +1,27 @@ { - "configPath": [ - "xpack", - "uptime" - ], + "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": [ - "cloud", - "data", - "fleet", - "home", - "ml" - ], + "optionalPlugins": ["cloud", "data", "fleet", "home", "ml"], "requiredPlugins": [ "alerting", "embeddable", "encryptedSavedObjects", - "inspector", "features", + "inspector", "licensing", "observability", "ruleRegistry", "security", + "share", + "taskManager", "triggersActionsUi", - "usageCollection", - "taskManager" + "usageCollection" ], "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": [ - "observability", - "kibanaReact", - "kibanaUtils", - "home", - "data", - "ml", - "fleet" - ], + "requiredBundles": ["data", "fleet", "home", "kibanaReact", "kibanaUtils", "ml", "observability"], "owner": { "name": "Uptime", "githubTeam": "uptime" diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.test.ts b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts new file mode 100644 index 0000000000000..c414778f7769c --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OVERVIEW_ROUTE } from '../../../common/constants'; +import { uptimeOverviewNavigatorParams } from './overview'; + +describe('uptimeOverviewNavigatorParams', () => { + it('supplies the correct app name', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({}); + expect(location.app).toEqual('uptime'); + }); + + it('creates the expected path when no params specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({}); + expect(location.path).toEqual(OVERVIEW_ROUTE); + }); + + it('creates a path with expected search when ip is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ ip: '127.0.0.1' }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=monitor.ip: "127.0.0.1"`); + }); + + it('creates a path with expected search when hostname is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ hostname: 'elastic.co' }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=url.domain: "elastic.co"`); + }); + + it('creates a path with expected search when multiple keys are specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ + hostname: 'elastic.co', + ip: '127.0.0.1', + }); + expect(location.path).toEqual( + `${OVERVIEW_ROUTE}?search=monitor.ip: "127.0.0.1" OR url.domain: "elastic.co"` + ); + }); +}); diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.ts b/x-pack/plugins/uptime/public/apps/locators/overview.ts new file mode 100644 index 0000000000000..d7faf7b78f797 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/locators/overview.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { uptimeOverviewLocatorID } from '../../../../observability/public'; +import { OVERVIEW_ROUTE } from '../../../common/constants'; + +const formatSearchKey = (key: string, value: string) => `${key}: "${value}"`; + +async function navigate({ ip, hostname }: { ip?: string; hostname?: string }) { + const searchParams: string[] = []; + + if (ip) searchParams.push(formatSearchKey('monitor.ip', ip)); + if (hostname) searchParams.push(formatSearchKey('url.domain', hostname)); + + const searchString = searchParams.join(' OR '); + + const path = + searchParams.length === 0 ? OVERVIEW_ROUTE : OVERVIEW_ROUTE + `?search=${searchString}`; + + return { + app: 'uptime', + path, + state: {}, + }; +} + +export const uptimeOverviewNavigatorParams = { + id: uptimeOverviewLocatorID, + getLocation: navigate, +}; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index ec6deef429ca9..dd2287b3b1642 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { CoreSetup, CoreStart, @@ -14,6 +15,7 @@ import { import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { SharePluginSetup, SharePluginStart } from '../../../../../src/plugins/share/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { @@ -29,6 +31,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; + import { alertTypeInitializers, legacyAlertTypeInitializers } from '../lib/alert_types'; import { FleetStart } from '../../../fleet/public'; import { @@ -47,19 +50,21 @@ import { Start as InspectorPluginStart } from '../../../../../src/plugins/inspec import { UptimeUiConfig } from '../../common/config'; export interface ClientPluginsSetup { - data: DataPublicPluginSetup; home?: HomePublicPluginSetup; + data: DataPublicPluginSetup; observability: ObservabilityPublicSetup; + share: SharePluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } export interface ClientPluginsStart { - embeddable: EmbeddableStart; - data: DataPublicPluginStart; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; fleet?: FleetStart; - observability: ObservabilityPublicStart; + data: DataPublicPluginStart; inspector: InspectorPluginStart; + embeddable: EmbeddableStart; + observability: ObservabilityPublicStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } export interface UptimePluginServices extends Partial { diff --git a/x-pack/plugins/uptime/public/apps/render_app.tsx b/x-pack/plugins/uptime/public/apps/render_app.tsx index cc831680dbf09..23f8fc9a8e58c 100644 --- a/x-pack/plugins/uptime/public/apps/render_app.tsx +++ b/x-pack/plugins/uptime/public/apps/render_app.tsx @@ -18,6 +18,7 @@ import { import { UptimeApp, UptimeAppProps } from './uptime_app'; import { ClientPluginsSetup, ClientPluginsStart } from './plugin'; import { UptimeUiConfig } from '../../common/config'; +import { uptimeOverviewNavigatorParams } from './locators/overview'; export function renderApp( core: CoreStart, @@ -41,6 +42,8 @@ export function renderApp( const canSave = (capabilities.uptime.save ?? false) as boolean; + plugins.share.url.locators.create(uptimeOverviewNavigatorParams); + const props: UptimeAppProps = { plugins, canSave, diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 9ff6701d20f7a..b0fe387613f40 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -9,14 +9,14 @@ import React from 'react'; import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useRouteMatch } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; -import { MONITOR_MANAGEMENT, SETTINGS_ROUTE } from '../../../../common/constants'; +import { MONITOR_MANAGEMENT, MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; import { InspectorHeaderLink } from './inspector_header_link'; import { monitorStatusSelector } from '../../../state/selectors'; @@ -44,6 +44,7 @@ export function ActionMenuContent({ config }: { config: UptimeConfig }): React.R const selectedMonitor = useSelector(monitorStatusSelector); + const detailRouteMatch = useRouteMatch(MONITOR_ROUTE); const monitorId = selectedMonitor?.monitor?.id; const syntheticExploratoryViewLink = createExploratoryViewUrl( @@ -57,7 +58,10 @@ export function ActionMenuContent({ config }: { config: UptimeConfig }): React.R time: { from: dateRangeStart, to: dateRangeEnd }, breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name ? [selectedMonitor?.monitor?.name] : [], + 'monitor.name': + selectedMonitor?.monitor?.name && detailRouteMatch?.isExact === true + ? [selectedMonitor?.monitor?.name] + : [], 'url.full': ['ALL_VALUES'], }, name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx index d352ccef51a94..14d3495cb96e2 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from '../../../lib/helper/rtl_helpers'; -import { DataStream, HTTPFields, ScheduleUnit } from '../../../../common/runtime_types'; +import { ConfigKey, DataStream, HTTPFields, ScheduleUnit } from '../../../../common/runtime_types'; import { MonitorManagementList } from './monitor_list'; import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; -describe('', () => { +describe('', () => { const setRefresh = jest.fn(); const setPageSize = jest.fn(); const setPageIndex = jest.fn(); @@ -110,4 +110,64 @@ describe('', () => { expect(setPageIndex).toBeCalledWith(2); expect(setRefresh).toBeCalledWith(true); }); + + it.each([ + [DataStream.BROWSER, ConfigKey.SOURCE_INLINE], + [DataStream.HTTP, ConfigKey.URLS], + [DataStream.TCP, ConfigKey.HOSTS], + [DataStream.ICMP, ConfigKey.HOSTS], + ])( + 'appends inline to the monitor id for browser monitors and omits for lightweight checks', + (type, configKey) => { + const id = '123456'; + const name = 'sample monitor'; + const browserState = { + monitorManagementList: { + ...state.monitorManagementList, + list: { + ...state.monitorManagementList.list, + monitors: [ + { + id, + attributes: { + name, + schedule: { + unit: ScheduleUnit.MINUTES, + number: '1', + }, + [configKey]: 'test', + type, + tags: [`tag-1`], + }, + }, + ], + }, + }, + }; + + render( + , + { state: browserState } + ); + + const link = screen.getByText(name) as HTMLAnchorElement; + + expect(link.href).toEqual( + expect.stringContaining( + `/app/uptime/monitor/${Buffer.from( + `${id}${type === DataStream.BROWSER ? `-inline` : ''}`, + 'utf8' + ).toString('base64')}` + ) + ); + + expect(setPageIndex).toBeCalledWith(2); + expect(setRefresh).toBeCalledWith(true); + } + ); }); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx index 75c94c2d07d1e..a0785df79bd75 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiPanel, EuiSpacer, EuiLink } from '@elastic/eui'; import { SyntheticsMonitorSavedObject } from '../../../../common/types'; import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; -import { MonitorFields, SyntheticsMonitor } from '../../../../common/runtime_types'; +import { DataStream, MonitorFields, SyntheticsMonitor } from '../../../../common/runtime_types'; import { UptimeSettingsContext } from '../../../contexts'; import { Actions } from './actions'; import { MonitorLocations } from './monitor_locations'; @@ -66,14 +66,19 @@ export const MonitorManagementList = ({ defaultMessage: 'Monitor name', }), render: ({ - attributes: { name }, + attributes: { name, type }, id, }: { attributes: Partial; id: string; }) => ( {name} diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx index 82917fc4e1758..9f3da1674ca09 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx @@ -6,10 +6,11 @@ */ import { useDispatch, useSelector } from 'react-redux'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { AlertTlsComponent } from '../alert_tls'; import { setAlertFlyoutVisible } from '../../../../state/actions'; import { selectDynamicSettings } from '../../../../state/selectors'; +import { getDynamicSettings } from '../../../../state/actions/dynamic_settings'; export const AlertTls: React.FC<{}> = () => { const dispatch = useDispatch(); @@ -18,6 +19,13 @@ export const AlertTls: React.FC<{}> = () => { [dispatch] ); const { settings } = useSelector(selectDynamicSettings); + + useEffect(() => { + if (typeof settings === 'undefined') { + dispatch(getDynamicSettings()); + } + }, [dispatch, settings]); + return ( { disabled={false} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > @@ -90,6 +91,7 @@ describe('AddFilterButton component', () => { disabled={false} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > @@ -143,6 +145,7 @@ describe('AddFilterButton component', () => { disabled={true} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx index 66f0f296b1248..58b8e7bb085da 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import * as labels from '../translations'; +import { useIndexPattern } from '../../../../contexts/uptime_index_pattern_context'; interface Props { newFilters: string[]; @@ -20,6 +21,8 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler const getSelectedItems = (fieldName: string) => alertFilters?.[fieldName] ?? []; + const indexPattern = useIndexPattern(); + const onButtonClick = () => { setPopover(!isPopoverOpen); }; @@ -62,6 +65,7 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler onClick={onButtonClick} size="s" flush="left" + isLoading={!indexPattern} > {labels.ADD_FILTER} diff --git a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx index 8171f7e19865f..6c658ec7f5d40 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import React, { createContext, useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { createContext, useContext } from 'react'; import { useFetcher } from '../../../observability/public'; import { DataPublicPluginStart, IndexPattern } from '../../../../../src/plugins/data/public'; -import { indexStatusSelector, selectDynamicSettings } from '../state/selectors'; -import { getDynamicSettings } from '../state/actions/dynamic_settings'; +import { useHasData } from '../components/overview/empty_state/use_has_data'; export const UptimeIndexPatternContext = createContext({} as IndexPattern); @@ -18,16 +16,7 @@ export const UptimeIndexPatternContextProvider: React.FC<{ data: DataPublicPlugi children, data: { indexPatterns }, }) => { - const { settings } = useSelector(selectDynamicSettings); - const { data: indexStatus } = useSelector(indexStatusSelector); - - const dispatch = useDispatch(); - - useEffect(() => { - if (typeof settings === 'undefined') { - dispatch(getDynamicSettings()); - } - }, [dispatch, settings]); + const { settings, data: indexStatus } = useHasData(); const heartbeatIndices = settings?.heartbeatIndices || ''; diff --git a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts index 024e387d23547..ffe7c61c7a4e3 100644 --- a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts +++ b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { takeLatest, put, call } from 'redux-saga/effects'; +import { takeLeading, put, call, takeLatest } from 'redux-saga/effects'; import { Action } from 'redux-actions'; import { i18n } from '@kbn/i18n'; import { fetchEffectFactory } from './fetch_effect'; @@ -25,7 +25,7 @@ import { DynamicSettings } from '../../../common/runtime_types'; import { kibanaService } from '../kibana_service'; export function* fetchDynamicSettingsEffect() { - yield takeLatest( + yield takeLeading( String(getDynamicSettings), fetchEffectFactory(getDynamicSettingsAPI, getDynamicSettingsSuccess, getDynamicSettingsFail) ); diff --git a/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts index 5aa6b7ea7c5a9..5fc99816df006 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts @@ -38,7 +38,7 @@ export const registerUptimeSavedObjects = ( }; export interface UMSavedObjectsAdapter { - config: UptimeConfig; + config: UptimeConfig | null; getUptimeDynamicSettings: UMSavedObjectsQueryFn; setUptimeDynamicSettings: UMSavedObjectsQueryFn; } diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts index f028d5e154a56..496f39557adb1 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts @@ -25,7 +25,10 @@ describe('getEsHostsTest', () => { it('should return expected host in cloud', function () { const esHosts = getEsHosts({ cloud: cloudSetup, - config: {}, + config: { + enabled: true, + manifestUrl: 'https://testing.com', + }, }); expect(esHosts).toEqual([ @@ -36,11 +39,9 @@ describe('getEsHostsTest', () => { it('should return expected host from config', function () { const esHosts = getEsHosts({ config: { - unsafe: { - service: { - hosts: ['http://localhost:9200'], - }, - }, + enabled: true, + manifestUrl: 'https://testing.com', + hosts: ['http://localhost:9200'], }, }); @@ -50,11 +51,9 @@ describe('getEsHostsTest', () => { const esHosts = getEsHosts({ cloud: cloudSetup, config: { - unsafe: { - service: { - hosts: ['http://localhost:9200'], - }, - }, + enabled: true, + manifestUrl: 'https://testing.com', + hosts: ['http://localhost:9200'], }, }); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts index d0de73b73e23e..847fcfa9db834 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts @@ -14,14 +14,14 @@ import { CloudSetup } from '../../../../cloud/server'; import { decodeCloudId } from '../../../../fleet/common'; -import { UptimeConfig } from '../../../common/config'; +import { ServiceConfig } from '../../../common/config'; export function getEsHosts({ cloud, config, }: { cloud?: CloudSetup; - config: UptimeConfig; + config: ServiceConfig; }): string[] { const cloudId = cloud?.isCloudEnabled && cloud.cloudId; const cloudUrl = cloudId && decodeCloudId(cloudId)?.elasticsearchUrl; @@ -30,7 +30,7 @@ export function getEsHosts({ return cloudHosts; } - const flagHosts = config?.unsafe?.service?.hosts; + const flagHosts = config.hosts; if (flagHosts && flagHosts.length > 0) { return flagHosts; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index 736e73da71134..d515e394cab7e 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -8,10 +8,13 @@ import axios from 'axios'; import { forkJoin, from as rxjsFrom, Observable, of } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; +import * as https from 'https'; +import { SslConfig } from '@kbn/server-http-tools'; import { getServiceLocations } from './get_service_locations'; import { Logger } from '../../../../../../src/core/server'; import { MonitorFields, ServiceLocations } from '../../../common/runtime_types'; import { convertToDataStreamFormat } from './formatters/convert_to_data_stream'; +import { ServiceConfig } from '../../../common/config'; const TEST_SERVICE_USERNAME = 'localKibanaIntegrationTestsUser'; @@ -24,14 +27,23 @@ export interface ServiceData { } export class ServiceAPIClient { - private readonly username: string; + private readonly username?: string; private readonly authorization: string; private locations: ServiceLocations; private logger: Logger; + private readonly config: ServiceConfig; - constructor(manifestUrl: string, username: string, password: string, logger: Logger) { + constructor(logger: Logger, config: ServiceConfig) { + this.config = config; + const { username, password, manifestUrl } = config; this.username = username; - this.authorization = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); + + if (username && password) { + this.authorization = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); + } else { + this.authorization = ''; + } + this.logger = logger; this.locations = []; @@ -40,6 +52,19 @@ export class ServiceAPIClient { }); } + getHttpsAgent() { + const config = this.config; + if (config.tls && config.tls.certificate && config.tls.key) { + const tlsConfig = new SslConfig(config.tls); + + return new https.Agent({ + rejectUnauthorized: true, // (NOTE: this will disable client verification) + cert: tlsConfig.certificate, + key: tlsConfig.key, + }); + } + } + async post(data: ServiceData) { return this.callAPI('POST', data); } @@ -68,9 +93,12 @@ export class ServiceAPIClient { method, url: url + '/monitors', data: { monitors: monitorsStreams, output }, - headers: { - Authorization: this.authorization, - }, + headers: this.authorization + ? { + Authorization: this.authorization, + } + : undefined, + httpsAgent: this.getHttpsAgent(), }); }; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index 82a901192b0ee..e25535df08baf 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -20,7 +20,7 @@ import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetic import { getAPIKeyForSyntheticsService } from './get_api_key'; import { syntheticsMonitorType } from '../saved_objects/synthetics_monitor'; import { getEsHosts } from './get_es_hosts'; -import { UptimeConfig } from '../../../common/config'; +import { ServiceConfig } from '../../../common/config'; import { ServiceAPIClient } from './service_api_client'; import { formatMonitorConfig } from './formatters/format_configs'; import { @@ -40,19 +40,17 @@ export class SyntheticsService { private readonly server: UptimeServerSetup; private apiClient: ServiceAPIClient; - private readonly config: UptimeConfig; + private readonly config: ServiceConfig; private readonly esHosts: string[]; private apiKey: SyntheticsServiceApiKey | undefined; - constructor(logger: Logger, server: UptimeServerSetup) { + constructor(logger: Logger, server: UptimeServerSetup, config: ServiceConfig) { this.logger = logger; this.server = server; - this.config = server.config; + this.config = config; - const { manifestUrl, username, password } = this.config.unsafe.service; - - this.apiClient = new ServiceAPIClient(manifestUrl, username, password, logger); + this.apiClient = new ServiceAPIClient(logger, this.config); this.esHosts = getEsHosts({ config: this.config, cloud: server.cloud }); } @@ -116,8 +114,7 @@ export class SyntheticsService { public async scheduleSyncTask( taskManager: TaskManagerStartContract ): Promise { - const interval = - this.config.unsafe.service.syncInterval ?? SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; + const interval = this.config.syncInterval ?? SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; try { await taskManager.removeIfExists(SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID); diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index 692607041ea80..4c076db0255ef 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { PluginInitializerContext, CoreStart, @@ -75,7 +74,12 @@ export class Plugin implements PluginType { } as UptimeServerSetup; if (this.server?.config?.unsafe?.service.enabled) { - this.syntheticService = new SyntheticsService(this.logger, this.server); + this.syntheticService = new SyntheticsService( + this.logger, + this.server, + this.server.config.unsafe.service + ); + this.syntheticService.registerSyncTask(plugins.taskManager); } @@ -111,7 +115,7 @@ export class Plugin implements PluginType { this.server.savedObjectsClient = this.savedObjectsClient; } - if (this.server?.config?.unsafe?.service.enabled) { + if (this.server?.config?.unsafe?.service?.enabled) { this.syntheticService?.init(); this.syntheticService?.scheduleSyncTask(plugins.taskManager); if (this.server && this.syntheticService) { diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts index ecf95c7e9175a..dfd0dcd1a9107 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts @@ -14,5 +14,5 @@ export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({ path: API_URLS.SERVICE_LOCATIONS, validate: {}, handler: async ({ server }): Promise => - getServiceLocations({ manifestUrl: server.config.unsafe.service.manifestUrl }), + getServiceLocations({ manifestUrl: server.config.unsafe!.service.manifestUrl }), }); diff --git a/x-pack/test/functional/apps/canvas/filters.ts b/x-pack/test/functional/apps/canvas/filters.ts index e5b97fa2350f1..ce8b319b9d53f 100644 --- a/x-pack/test/functional/apps/canvas/filters.ts +++ b/x-pack/test/functional/apps/canvas/filters.ts @@ -43,16 +43,18 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro // Double check that the filter has the correct time range and default filter value const startingMatchFilters = await PageObjects.canvas.getMatchFiltersFromDebug(); - expect(startingMatchFilters[0].value).to.equal('apm'); - expect(startingMatchFilters[0].column).to.equal('project'); + const projectQuery = startingMatchFilters[0].query.term.project; + expect(projectQuery !== null && typeof projectQuery === 'object').to.equal(true); + expect(projectQuery?.value).to.equal('apm'); // Change dropdown value await testSubjects.selectValue('canvasDropdownFilter__select', 'beats'); await retry.try(async () => { const matchFilters = await PageObjects.canvas.getMatchFiltersFromDebug(); - expect(matchFilters[0].value).to.equal('beats'); - expect(matchFilters[0].column).to.equal('project'); + const newProjectQuery = matchFilters[0].query.term.project; + expect(newProjectQuery !== null && typeof newProjectQuery === 'object').to.equal(true); + expect(newProjectQuery?.value).to.equal('beats'); }); }); @@ -66,18 +68,20 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro }); const startingTimeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(startingTimeFilters[0].column).to.equal('@timestamp'); - expect(new Date(startingTimeFilters[0].from).toDateString()).to.equal('Sun Oct 18 2020'); - expect(new Date(startingTimeFilters[0].to).toDateString()).to.equal('Sat Oct 24 2020'); + const timestampQuery = startingTimeFilters[0].query.range['@timestamp']; + expect(timestampQuery !== null && typeof timestampQuery === 'object').to.equal(true); + expect(new Date(timestampQuery.gte).toDateString()).to.equal('Sun Oct 18 2020'); + expect(new Date(timestampQuery.lte).toDateString()).to.equal('Sat Oct 24 2020'); await testSubjects.click('superDatePickerstartDatePopoverButton'); await find.clickByCssSelector('.react-datepicker [aria-label="day-19"]', 20000); await retry.try(async () => { const timeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(timeFilters[0].column).to.equal('@timestamp'); - expect(new Date(timeFilters[0].from).toDateString()).to.equal('Mon Oct 19 2020'); - expect(new Date(timeFilters[0].to).toDateString()).to.equal('Sat Oct 24 2020'); + const newTimestampQuery = timeFilters[0].query.range['@timestamp']; + expect(newTimestampQuery !== null && typeof newTimestampQuery === 'object').to.equal(true); + expect(new Date(newTimestampQuery.gte).toDateString()).to.equal('Mon Oct 19 2020'); + expect(new Date(newTimestampQuery.lte).toDateString()).to.equal('Sat Oct 24 2020'); }); await testSubjects.click('superDatePickerendDatePopoverButton'); @@ -85,9 +89,10 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro await retry.try(async () => { const timeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(timeFilters[0].column).to.equal('@timestamp'); - expect(new Date(timeFilters[0].from).toDateString()).to.equal('Mon Oct 19 2020'); - expect(new Date(timeFilters[0].to).toDateString()).to.equal('Fri Oct 23 2020'); + const newTimestampQuery = timeFilters[0].query.range['@timestamp']; + expect(newTimestampQuery !== null && typeof newTimestampQuery === 'object').to.equal(true); + expect(new Date(newTimestampQuery.gte).toDateString()).to.equal('Mon Oct 19 2020'); + expect(new Date(newTimestampQuery.lte).toDateString()).to.equal('Fri Oct 23 2020'); }); }); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 1d5c6d1bf84a3..7a84c41aa4a66 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - // FLAKY: https://github.com/elastic/kibana/issues/122927 - describe.skip('regression creation', function () { + describe('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index 2b570a4d7dae6..a51b878b6af30 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -108,7 +108,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo const filters = JSON.parse(content); - return filters.and.filter((f: any) => f.filterType === 'time'); + return filters.filters.filter((f: any) => f.query?.range); }, async getMatchFiltersFromDebug() { @@ -119,7 +119,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo const filters = JSON.parse(content); - return filters.and.filter((f: any) => f.filterType === 'exactly'); + return filters.filters.filter((f: any) => f.query?.term); }, async clickAddFromLibrary() { diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index 15c87ea450425..29faf3cee3b51 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -32,7 +32,9 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr const $ = await row.parseDomContent(); const viewCell = await row.findByTestSubject('sessionManagementNameCol'); const actionsCell = await row.findByTestSubject('sessionManagementActionsCol'); + return { + id: (await row.getAttribute('data-test-search-session-id')).split('id-')[1], name: $.findTestSubject('sessionManagementNameCol').text().trim(), status: $.findTestSubject('sessionManagementStatusLabel').attr('data-test-status'), mainUrl: $.findTestSubject('sessionManagementNameCol').text(), diff --git a/x-pack/test/functional/services/ml/data_frame_analytics.ts b/x-pack/test/functional/services/ml/data_frame_analytics.ts index aafe96c2c4967..97834bc57c4ab 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics.ts @@ -51,13 +51,15 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async startAnalyticsCreation() { - await retry.tryForTime(20 * 1000, async () => { - if (await testSubjects.exists('mlNoDataFrameAnalyticsFound', { timeout: 1000 })) { + await retry.tryForTime(30 * 1000, async () => { + if (await testSubjects.exists('mlAnalyticsCreateFirstButton', { timeout: 1000 })) { await testSubjects.click('mlAnalyticsCreateFirstButton'); - } else { + } else if (await testSubjects.exists('mlAnalyticsButtonCreate', { timeout: 1000 })) { await testSubjects.click('mlAnalyticsButtonCreate'); + } else { + throw new Error('No Analytics create button found'); } - await testSubjects.existOrFail('analyticsCreateSourceIndexModal'); + await testSubjects.existOrFail('analyticsCreateSourceIndexModal', { timeout: 5000 }); }); }, diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index 8561094890474..308dc472c29fe 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -18,17 +19,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'security', 'timePicker', + 'searchSessionsManagement', ]); const dashboardPanelActions = getService('dashboardPanelActions'); const browser = getService('browser'); const searchSessions = getService('searchSessions'); + const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); - // Failing: See https://github.com/elastic/kibana/issues/112732 - describe.skip('dashboard in space', () => { + describe('dashboard in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -63,6 +75,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); @@ -70,11 +84,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard', { basePath: 's/another-space' }); await PageObjects.dashboard.loadSavedDashboard('A Dashboard in another space'); - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 1, 2015 @ 00:00:00.000', - 'Oct 1, 2015 @ 00:00:00.000' - ); - await PageObjects.dashboard.waitForRenderComplete(); await searchSessions.expectState('completed'); @@ -84,16 +93,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'A Pie in another space' ); - // load URL to restore a saved session - const url = await browser.getCurrentUrl(); - const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; - await browser.get(savedSessionURL); + await searchSessions.openPopover(); + await searchSessions.viewSearchSessions(); + + // purge client side search cache + // https://github.com/elastic/kibana/issues/106074#issuecomment-920462094 + await browser.refresh(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + const searchSessionItem = searchSessionList.find( + (session) => session.id === savedSessionId + ); + + if (!searchSessionItem) throw new Error(`Can\'t find session with id = ${savedSessionId}`); + + // navigate to discover + await searchSessionItem.view(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); // Check that session is restored await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); + expect(await toasts.getToastCount()).to.be(0); // no session restoration related warnings }); }); @@ -101,6 +124,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -135,6 +167,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); @@ -142,11 +176,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard', { basePath: 's/another-space' }); await PageObjects.dashboard.loadSavedDashboard('A Dashboard in another space'); - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 1, 2015 @ 00:00:00.000', - 'Oct 1, 2015 @ 00:00:00.000' - ); - await PageObjects.dashboard.waitForRenderComplete(); await searchSessions.expectState('completed'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts index b989ad1127306..922ecfc12dc4f 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -19,16 +20,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'security', 'timePicker', + 'searchSessionsManagement', ]); const browser = getService('browser'); const searchSessions = getService('searchSessions'); + const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); - // FLAKY https://github.com/elastic/kibana/issues/112913 - describe.skip('discover in space', () => { + describe('discover in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -63,6 +75,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); @@ -71,11 +85,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.selectIndexPattern('logstash-*'); - await PageObjects.timePicker.setAbsoluteRange( - 'Sep 1, 2015 @ 00:00:00.000', - 'Oct 1, 2015 @ 00:00:00.000' - ); - await PageObjects.discover.waitForDocTableLoadingComplete(); await searchSessions.expectState('completed'); @@ -88,22 +97,45 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).getAttribute('data-search-session-id'); await inspector.close(); - // load URL to restore a saved session - const url = await browser.getCurrentUrl(); - const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; - await browser.get(savedSessionURL); + await searchSessions.openPopover(); + await searchSessions.viewSearchSessions(); + + // purge client side search cache + // https://github.com/elastic/kibana/issues/106074#issuecomment-920462094 + await browser.refresh(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + const searchSessionItem = searchSessionList.find( + (session) => session.id === savedSessionId + ); + + if (!searchSessionItem) throw new Error(`Can\'t find session with id = ${savedSessionId}`); + + // navigate to discover + await searchSessionItem.view(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.waitForDocTableLoadingComplete(); // Check that session is restored await searchSessions.expectState('restored'); - await testSubjects.missingOrFail('discoverNoResultsError'); // expect error because of fake searchSessionId + await testSubjects.missingOrFail('discoverNoResultsError'); + expect(await toasts.getToastCount()).to.be(0); // no session restoration related warnings }); }); describe('Disabled storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); + await kibanaServer.uiSettings.replace( + { + 'timepicker:timeDefaults': + '{ "from": "2015-09-01T00:00:00.000Z", "to": "2015-10-01T00:00:00.000Z"}', + defaultIndex: 'd1bd6c84-d9d0-56fb-8a72-63fe60020920', + }, + { space: 'another-space' } + ); + await security.role.create('data_analyst', { elasticsearch: { indices: [{ names: ['logstash-*'], privileges: ['all'] }], @@ -138,6 +170,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.role.delete('data_analyst'); await security.user.delete('analyst'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults', { space: 'another-space' }); + await kibanaServer.uiSettings.unset('defaultIndex', { space: 'another-space' }); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); }); diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json b/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json index b7de2dba02d19..bc3c1c302c685 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions/data.json @@ -8,6 +8,9 @@ "agent": { "name": "bond" }, + "unique_value": { + "test": "test field" + }, "user" : [ { "name" : "john", diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json b/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json index e63b86392756f..3b5cc2dae545c 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions/mappings.json @@ -25,6 +25,14 @@ } } }, + "unique_value": { + "properties": { + "test": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "user": { "type": "nested", "properties": {