diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 8f4301114a11..c1535e8a2146 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -216,7 +216,7 @@ generating deep links to other apps, and creating short URLs. |{kib-repo}blob/{branch}/src/plugins/telemetry_management_section/README.md[telemetryManagementSection] -|This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). +|This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry). |{kib-repo}blob/{branch}/src/plugins/tile_map/README.md[tileMap] diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index 69f9d380422b..1bee15525025 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -22,11 +22,12 @@ export interface IExpressionLoaderParams | [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | ExpressionRenderHandlerParams['hasCompatibleActions'] | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | -| [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | boolean | | +| [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | boolean | The flag to toggle on emitting partial results. By default, the partial results are disabled. | | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | | [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | | [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) | boolean | | +| [throttle](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md) | number | Throttling of partial results in milliseconds. By default, throttling is disabled. | | [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | unknown | | | [variables](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.variables.md) | Record<string, any> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md index 84c42c3f59f2..8922b2d0f377 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md @@ -4,6 +4,8 @@ ## IExpressionLoaderParams.partial property +The flag to toggle on emitting partial results. By default, the partial results are disabled. + Signature: ```typescript diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md new file mode 100644 index 000000000000..3383bce87977 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [throttle](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md) + +## IExpressionLoaderParams.throttle property + +Throttling of partial results in milliseconds. By default, throttling is disabled. + +Signature: + +```typescript +throttle?: number; +``` diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index bd714c62ff54..a0dd8750ffc8 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -337,24 +337,12 @@ For more details and a reference of audit events, refer to <>, specify where you want to write the audit events using `xpack.security.audit.appender`. - -[cols="2*<,*50"] -|====== -| `xpack.security.audit.appender` -| Optional. Specifies where audit logs should be written to and how they should be formatted. +| Set to `true` _and_ configure an appender with `xpack.security.audit.appender` to enable ECS audit logging`. *Default:* `false` 2+a| For example: - [source,yaml] ---------------------------------------- +xpack.security.audit.enabled: true xpack.security.audit.appender: type: rolling-file fileName: ./audit.log @@ -370,7 +358,31 @@ xpack.security.audit.appender: <1> Rotates log files every 24 hours. <2> Keeps maximum of 10 log files before deleting older ones. -| `xpack.security.audit.appender.type` +[NOTE] +============ +{ess} does not support custom log file policies. To enable audit logging on {ess} only specify: + +[source,yaml] +---------------------------------------- +xpack.security.audit.enabled: true +xpack.security.audit.appender.type: rolling-file +---------------------------------------- +============ + +[NOTE] +============ +deprecated:[7.15.0,"In 8.0 and later, the legacy audit logger will be removed, and this setting will enable the ECS audit logger with a default appender."] To enable the legacy audit logger only specify: + +[source,yaml] +---------------------------------------- +xpack.security.audit.enabled: true +---------------------------------------- +============ + +| `xpack.security.audit.appender` {ess-icon} +| Optional. Specifies where audit logs should be written to and how they should be formatted. + +| `xpack.security.audit.appender.type` {ess-icon} | Required. Specifies where audit logs should be written to. Allowed values are `console`, `file`, or `rolling-file`. Refer to <> and <> for appender specific settings. diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index db40feab20ce..e2f21e3f8470 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -14,16 +14,24 @@ by cluster-wide privileges. For more information on enabling audit logging in [IMPORTANT] ============================================================================ +Kibana offers two audit logs: a **deprecated** legacy audit logger, and a new +ECS-compliant audit logger. We strongly advise using the <>, +as the legacy audit logger will be removed in an upcoming version. +============================================================================ + +[NOTE] +============================================================================ Audit logs are **disabled** by default. To enable this functionality, you must -set `xpack.security.audit.enabled` to `true` in `kibana.yml`. +set `xpack.security.audit.enabled` to `true` in `kibana.yml`, and configure +an <> to write the audit log to a location of your choosing. ============================================================================ -The current version of the audit logger uses the standard {kib} logging output, +The legacy audit logger uses the standard {kib} logging output, which can be configured in `kibana.yml`. For more information, refer to <>. -The audit logger uses a separate logger and can be configured using +The <> uses a separate logger and can be configured using the options in <>. -==== Audit event types +==== Legacy audit event types When you are auditing security events, each request can generate multiple audit events. The following is a list of the events that can be generated: @@ -42,7 +50,7 @@ events. The following is a list of the events that can be generated: ============================================================================ The following events are only logged if the ECS audit logger is enabled. For information on how to configure `xpack.security.audit.appender`, refer to -<>. +<>. ============================================================================ Refer to the table of events that can be logged for auditing purposes. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index d38c3aa34614..2f6765cd57b9 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -107,7 +107,7 @@ pageLoadAssetSize: dataVisualizer: 27530 banners: 17946 mapsEms: 26072 - timelines: 230410 + timelines: 251886 screenshotMode: 17856 visTypePie: 35583 expressionRevealImage: 25675 diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 6c45403fc0a1..3cabb307b165 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -8,7 +8,7 @@ import { ValuesType } from 'utility-types'; -const ALERT_NAMESPACE = 'kibana.rac.alert'; +const ALERT_NAMESPACE = 'kibana.rac.alert' as const; const TIMESTAMP = '@timestamp' as const; const EVENT_KIND = 'event.kind' as const; @@ -28,6 +28,7 @@ const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; const ALERT_SEVERITY_LEVEL = `${ALERT_NAMESPACE}.severity.level` as const; const ALERT_SEVERITY_VALUE = `${ALERT_NAMESPACE}.severity.value` as const; const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; +const SPACE_IDS = 'kibana.space_ids' as const; const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; @@ -52,6 +53,7 @@ const fields = { ALERT_STATUS, ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, + SPACE_IDS, }; export { @@ -75,6 +77,7 @@ export { ALERT_STATUS, ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, + SPACE_IDS, }; export type TechnicalRuleDataFieldName = ValuesType; diff --git a/src/dev/ci_setup/load_env_keys.sh b/src/dev/ci_setup/load_env_keys.sh index 4b2ccb323065..5f7a6c26bab2 100644 --- a/src/dev/ci_setup/load_env_keys.sh +++ b/src/dev/ci_setup/load_env_keys.sh @@ -37,12 +37,6 @@ else KIBANA_BUILDBUDDY_CI_API_KEY=$(retry 5 vault read -field=value secret/kibana-issues/dev/kibana-buildbuddy-ci-api-key) export KIBANA_BUILDBUDDY_CI_API_KEY - # read FullStory env vars - FULLSTORY_ORG_ID=$(retry 5 vault read -field=org_id secret/kibana-issues/dev/fullstory-credentials) - export FULLSTORY_ORG_ID - FULLSTORY_API_KEY=$(retry 5 vault read -field=api_key secret/kibana-issues/dev/fullstory-credentials) - export FULLSTORY_API_KEY - # remove vault related secrets unset VAULT_ROLE_ID VAULT_SECRET_ID VAULT_TOKEN VAULT_ADDR fi diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts index c331ba6b4b9a..f5cb7e957471 100644 --- a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import { Observable } from 'rxjs'; import { Datatable, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; @@ -22,7 +23,7 @@ import { handleRequest } from './request_handler'; const name = 'esaggs'; type Input = KibanaContext | null; -type Output = Promise; +type Output = Observable; interface Arguments { index: IndexPatternExpressionType; diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts index 4f255cf4c244..dae3661f00c2 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { from } from 'rxjs'; import type { MockedKeys } from '@kbn/utility-types/jest'; import type { Filter } from '../../../es_query'; import type { IndexPattern } from '../../../index_patterns'; @@ -21,6 +22,7 @@ jest.mock('../../tabify', () => ({ import { tabifyAggResponse } from '../../tabify'; import { of } from 'rxjs'; +import { toArray } from 'rxjs/operators'; describe('esaggs expression function - public', () => { let mockParams: MockedKeys; @@ -57,7 +59,7 @@ describe('esaggs expression function - public', () => { }); test('should create a new search source instance', async () => { - await handleRequest(mockParams); + await handleRequest(mockParams).toPromise(); expect(mockParams.searchSourceService.create).toHaveBeenCalledTimes(1); }); @@ -65,7 +67,7 @@ describe('esaggs expression function - public', () => { let searchSource: MockedKeys; beforeEach(async () => { - await handleRequest(mockParams); + await handleRequest(mockParams).toPromise(); searchSource = await mockParams.searchSourceService.create(); }); @@ -100,7 +102,7 @@ describe('esaggs expression function - public', () => { await handleRequest({ ...mockParams, filters: mockFilters, - }); + }).toPromise(); searchSource = await mockParams.searchSourceService.create(); expect((searchSource.setField as jest.Mock).mock.calls[3]).toEqual(['filter', mockFilters]); }); @@ -118,14 +120,14 @@ describe('esaggs expression function - public', () => { await handleRequest({ ...mockParams, query: mockQuery, - }); + }).toPromise(); searchSource = await mockParams.searchSourceService.create(); expect((searchSource.setField as jest.Mock).mock.calls[4]).toEqual(['query', mockQuery]); }); }); test('calls searchSource.fetch', async () => { - await handleRequest(mockParams); + await handleRequest(mockParams).toPromise(); const searchSource = await mockParams.searchSourceService.create(); expect(searchSource.fetch$).toHaveBeenCalledWith({ @@ -140,7 +142,7 @@ describe('esaggs expression function - public', () => { }); test('tabifies response data', async () => { - await handleRequest(mockParams); + await handleRequest(mockParams).toPromise(); expect(tabifyAggResponse).toHaveBeenCalledWith( mockParams.aggs, {}, @@ -155,7 +157,7 @@ describe('esaggs expression function - public', () => { await handleRequest({ ...mockParams, timeRange: { from: '2020-12-01', to: '2020-12-31' }, - }); + }).toPromise(); expect((tabifyAggResponse as jest.Mock).mock.calls[0][2].timeRange).toMatchInlineSnapshot(` Object { "from": "2020-12-01T05:00:00.000Z", @@ -167,4 +169,29 @@ describe('esaggs expression function - public', () => { } `); }); + + test('returns partial results', async () => { + const searchSource = await mockParams.searchSourceService.create(); + + (searchSource.fetch$ as jest.MockedFunction).mockReturnValue( + from([ + { + rawResponse: {}, + }, + { + rawResponse: {}, + }, + ]) as ReturnType + ); + + const result = await handleRequest({ + ...mockParams, + query: { query: 'foo', language: 'bar' }, + }) + .pipe(toArray()) + .toPromise(); + + expect(result).toHaveLength(2); + expect(tabifyAggResponse).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index bf931966f5ba..f697138b1361 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -7,6 +7,8 @@ */ import { i18n } from '@kbn/i18n'; +import { defer } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { Adapters } from 'src/plugins/inspector/common'; import { calculateBounds, Filter, IndexPattern, Query, TimeRange } from '../../../../common'; @@ -32,7 +34,7 @@ export interface RequestHandlerParams { getNow?: () => Date; } -export const handleRequest = async ({ +export const handleRequest = ({ abortSignal, aggs, filters, @@ -46,87 +48,95 @@ export const handleRequest = async ({ timeRange, getNow, }: RequestHandlerParams) => { - const forceNow = getNow?.(); - const searchSource = await searchSourceService.create(); - - searchSource.setField('index', indexPattern); - searchSource.setField('size', 0); - - // Create a new search source that inherits the original search source - // but has the appropriate timeRange applied via a filter. - // This is a temporary solution until we properly pass down all required - // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641). - // Using callParentStartHandlers: true we make sure, that the parent searchSource - // onSearchRequestStart will be called properly even though we use an inherited - // search source. - const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); - const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); - - // If timeFields have been specified, use the specified ones, otherwise use primary time field of index - // pattern if it's available. - const defaultTimeField = indexPattern?.getTimeField?.(); - const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; - const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; - - aggs.setTimeRange(timeRange as TimeRange); - aggs.setForceNow(forceNow); - aggs.setTimeFields(allTimeFields); - - // For now we need to mirror the history of the passed search source, since - // the request inspector wouldn't work otherwise. - Object.defineProperty(requestSearchSource, 'history', { - get() { - return searchSource.history; - }, - set(history) { - return (searchSource.history = history); - }, - }); - - requestSearchSource.setField('aggs', aggs); - - requestSearchSource.onRequestStart((paramSearchSource, options) => { - return aggs.onSearchRequestStart(paramSearchSource, options); - }); - - // If a timeRange has been specified and we had at least one timeField available, create range - // filters for that those time fields - if (timeRange && allTimeFields.length > 0) { - timeFilterSearchSource.setField('filter', () => { - return aggs.getSearchSourceTimeFilter(forceNow); + return defer(async () => { + const forceNow = getNow?.(); + const searchSource = await searchSourceService.create(); + + searchSource.setField('index', indexPattern); + searchSource.setField('size', 0); + + // Create a new search source that inherits the original search source + // but has the appropriate timeRange applied via a filter. + // This is a temporary solution until we properly pass down all required + // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641). + // Using callParentStartHandlers: true we make sure, that the parent searchSource + // onSearchRequestStart will be called properly even though we use an inherited + // search source. + const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); + const requestSearchSource = timeFilterSearchSource.createChild({ + callParentStartHandlers: true, }); - } - - requestSearchSource.setField('filter', filters); - requestSearchSource.setField('query', query); - - const { rawResponse: response } = await requestSearchSource - .fetch$({ - abortSignal, - sessionId: searchSessionId, - inspector: { - adapter: inspectorAdapters.requests, - title: i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { - defaultMessage: 'Data', - }), - description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), + + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields?.length ? timeFields : defaultTimeFields; + + aggs.setTimeRange(timeRange as TimeRange); + aggs.setForceNow(forceNow); + aggs.setTimeFields(allTimeFields); + + // For now we need to mirror the history of the passed search source, since + // the request inspector wouldn't work otherwise. + Object.defineProperty(requestSearchSource, 'history', { + get() { + return searchSource.history; + }, + set(history) { + return (searchSource.history = history); }, - }) - .toPromise(); + }); - const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null; - const tabifyParams = { - metricsAtAllLevels: aggs.hierarchical, - partialRows, - timeRange: parsedTimeRange - ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } - : undefined, - }; + requestSearchSource.setField('aggs', aggs); - const tabifiedResponse = tabifyAggResponse(aggs, response, tabifyParams); + requestSearchSource.onRequestStart((paramSearchSource, options) => { + return aggs.onSearchRequestStart(paramSearchSource, options); + }); - return tabifiedResponse; + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (timeRange && allTimeFields.length > 0) { + timeFilterSearchSource.setField('filter', () => { + return aggs.getSearchSourceTimeFilter(forceNow); + }); + } + + requestSearchSource.setField('filter', filters); + requestSearchSource.setField('query', query); + + return { allTimeFields, forceNow, requestSearchSource }; + }).pipe( + switchMap(({ allTimeFields, forceNow, requestSearchSource }) => + requestSearchSource + .fetch$({ + abortSignal, + sessionId: searchSessionId, + inspector: { + adapter: inspectorAdapters.requests, + title: i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + }, + }) + .pipe( + map(({ rawResponse: response }) => { + const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null; + const tabifyParams = { + metricsAtAllLevels: aggs.hierarchical, + partialRows, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, + }; + + return tabifyAggResponse(aggs, response, tabifyParams); + }) + ) + ) + ); }; diff --git a/src/plugins/data/public/search/expressions/esaggs.test.ts b/src/plugins/data/public/search/expressions/esaggs.test.ts index e75bd7be219d..11dfe67838d3 100644 --- a/src/plugins/data/public/search/expressions/esaggs.test.ts +++ b/src/plugins/data/public/search/expressions/esaggs.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { of as mockOf } from 'rxjs'; import type { MockedKeys } from '@kbn/utility-types/jest'; import type { ExecutionContext } from 'src/plugins/expressions/public'; import type { IndexPatternsContract } from '../../../common/index_patterns/index_patterns'; @@ -20,7 +21,7 @@ import { getFunctionDefinition } from './esaggs'; jest.mock('../../../common/search/expressions', () => ({ getEsaggsMeta: jest.fn().mockReturnValue({ name: 'esaggs' }), - handleEsaggsRequest: jest.fn().mockResolvedValue({}), + handleEsaggsRequest: jest.fn(() => mockOf({})), })); import { getEsaggsMeta, handleEsaggsRequest } from '../../../common/search/expressions'; @@ -74,13 +75,13 @@ describe('esaggs expression function - public', () => { }); test('calls indexPatterns.create with the values provided by the subexpression arg', async () => { - await definition().fn(null, args, mockHandlers); + await definition().fn(null, args, mockHandlers).toPromise(); expect(startDependencies.indexPatterns.create).toHaveBeenCalledWith(args.index.value, true); }); test('calls aggs.createAggConfigs with the values provided by the subexpression arg', async () => { - await definition().fn(null, args, mockHandlers); + await definition().fn(null, args, mockHandlers).toPromise(); expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith( {}, @@ -96,7 +97,7 @@ describe('esaggs expression function - public', () => { }); test('calls handleEsaggsRequest with all of the right dependencies', async () => { - await definition().fn(null, args, mockHandlers); + await definition().fn(null, args, mockHandlers).toPromise(); expect(handleEsaggsRequest).toHaveBeenCalledWith({ abortSignal: mockHandlers.abortSignal, @@ -128,7 +129,7 @@ describe('esaggs expression function - public', () => { timeRange: { from: 'a', to: 'b' }, } as KibanaContext; - await definition().fn(input, args, mockHandlers); + await definition().fn(input, args, mockHandlers).toPromise(); expect(handleEsaggsRequest).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 1e3d56c71e42..6d658d44980d 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -7,6 +7,8 @@ */ import { get } from 'lodash'; +import { defer } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { StartServicesAccessor } from 'src/core/public'; import { EsaggsExpressionFunctionDefinition, @@ -35,30 +37,36 @@ export function getFunctionDefinition({ }) { return (): EsaggsExpressionFunctionDefinition => ({ ...getEsaggsMeta(), - async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { - const { aggs, indexPatterns, searchSource, getNow } = await getStartDependencies(); + fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { + return defer(async () => { + const { aggs, indexPatterns, searchSource, getNow } = await getStartDependencies(); - const indexPattern = await indexPatterns.create(args.index.value, true); - const aggConfigs = aggs.createAggConfigs( - indexPattern, - args.aggs!.map((agg) => agg.value) - ); - aggConfigs.hierarchical = args.metricsAtAllLevels; + const indexPattern = await indexPatterns.create(args.index.value, true); + const aggConfigs = aggs.createAggConfigs( + indexPattern, + args.aggs!.map((agg) => agg.value) + ); + aggConfigs.hierarchical = args.metricsAtAllLevels; - return await handleEsaggsRequest({ - abortSignal, - aggs: aggConfigs, - filters: get(input, 'filters', undefined), - indexPattern, - inspectorAdapters, - partialRows: args.partialRows, - query: get(input, 'query', undefined) as any, - searchSessionId: getSearchSessionId(), - searchSourceService: searchSource, - timeFields: args.timeFields, - timeRange: get(input, 'timeRange', undefined), - getNow, - }); + return { aggConfigs, indexPattern, searchSource, getNow }; + }).pipe( + switchMap(({ aggConfigs, indexPattern, searchSource, getNow }) => + handleEsaggsRequest({ + abortSignal, + aggs: aggConfigs, + filters: get(input, 'filters', undefined), + indexPattern, + inspectorAdapters, + partialRows: args.partialRows, + query: get(input, 'query', undefined) as any, + searchSessionId: getSearchSessionId(), + searchSourceService: searchSource, + timeFields: args.timeFields, + timeRange: get(input, 'timeRange', undefined), + getNow, + }) + ) + ); }, }); } diff --git a/src/plugins/data/server/search/expressions/esaggs.test.ts b/src/plugins/data/server/search/expressions/esaggs.test.ts index 15287e9d8cf5..7c1f7626f491 100644 --- a/src/plugins/data/server/search/expressions/esaggs.test.ts +++ b/src/plugins/data/server/search/expressions/esaggs.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { of as mockOf } from 'rxjs'; import type { MockedKeys } from '@kbn/utility-types/jest'; import { KibanaRequest } from 'src/core/server'; import type { ExecutionContext } from 'src/plugins/expressions/server'; @@ -21,7 +22,7 @@ import { getFunctionDefinition } from './esaggs'; jest.mock('../../../common/search/expressions', () => ({ getEsaggsMeta: jest.fn().mockReturnValue({ name: 'esaggs' }), - handleEsaggsRequest: jest.fn().mockResolvedValue({}), + handleEsaggsRequest: jest.fn(() => mockOf({})), })); import { getEsaggsMeta, handleEsaggsRequest } from '../../../common/search/expressions'; @@ -76,19 +77,19 @@ describe('esaggs expression function - server', () => { }); test('calls getStartDependencies with the KibanaRequest', async () => { - await definition().fn(null, args, mockHandlers); + await definition().fn(null, args, mockHandlers).toPromise(); expect(getStartDependencies).toHaveBeenCalledWith({ id: 'hi' }); }); test('calls indexPatterns.create with the values provided by the subexpression arg', async () => { - await definition().fn(null, args, mockHandlers); + await definition().fn(null, args, mockHandlers).toPromise(); expect(startDependencies.indexPatterns.create).toHaveBeenCalledWith(args.index.value, true); }); test('calls aggs.createAggConfigs with the values provided by the subexpression arg', async () => { - await definition().fn(null, args, mockHandlers); + await definition().fn(null, args, mockHandlers).toPromise(); expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith( {}, @@ -104,7 +105,7 @@ describe('esaggs expression function - server', () => { }); test('calls handleEsaggsRequest with all of the right dependencies', async () => { - await definition().fn(null, args, mockHandlers); + await definition().fn(null, args, mockHandlers).toPromise(); expect(handleEsaggsRequest).toHaveBeenCalledWith({ abortSignal: mockHandlers.abortSignal, @@ -135,7 +136,7 @@ describe('esaggs expression function - server', () => { timeRange: { from: 'a', to: 'b' }, } as KibanaContext; - await definition().fn(input, args, mockHandlers); + await definition().fn(input, args, mockHandlers).toPromise(); expect(handleEsaggsRequest).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/src/plugins/data/server/search/expressions/esaggs.ts b/src/plugins/data/server/search/expressions/esaggs.ts index bb22a491b157..3a39276c8ed4 100644 --- a/src/plugins/data/server/search/expressions/esaggs.ts +++ b/src/plugins/data/server/search/expressions/esaggs.ts @@ -7,6 +7,8 @@ */ import { get } from 'lodash'; +import { defer } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, StartServicesAccessor } from 'src/core/server'; import { @@ -36,45 +38,47 @@ export function getFunctionDefinition({ }): () => EsaggsExpressionFunctionDefinition { return () => ({ ...getEsaggsMeta(), - async fn( - input, - args, - { inspectorAdapters, abortSignal, getSearchSessionId, getKibanaRequest } - ) { - const kibanaRequest = getKibanaRequest ? getKibanaRequest() : null; - if (!kibanaRequest) { - throw new Error( - i18n.translate('data.search.esaggs.error.kibanaRequest', { - defaultMessage: - 'A KibanaRequest is required to execute this search on the server. ' + - 'Please provide a request object to the expression execution params.', - }) - ); - } + fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId, getKibanaRequest }) { + return defer(async () => { + const kibanaRequest = getKibanaRequest ? getKibanaRequest() : null; + if (!kibanaRequest) { + throw new Error( + i18n.translate('data.search.esaggs.error.kibanaRequest', { + defaultMessage: + 'A KibanaRequest is required to execute this search on the server. ' + + 'Please provide a request object to the expression execution params.', + }) + ); + } - const { aggs, indexPatterns, searchSource } = await getStartDependencies(kibanaRequest); + const { aggs, indexPatterns, searchSource } = await getStartDependencies(kibanaRequest); - const indexPattern = await indexPatterns.create(args.index.value, true); - const aggConfigs = aggs.createAggConfigs( - indexPattern, - args.aggs!.map((agg) => agg.value) - ); + const indexPattern = await indexPatterns.create(args.index.value, true); + const aggConfigs = aggs.createAggConfigs( + indexPattern, + args.aggs!.map((agg) => agg.value) + ); - aggConfigs.hierarchical = args.metricsAtAllLevels; + aggConfigs.hierarchical = args.metricsAtAllLevels; - return await handleEsaggsRequest({ - abortSignal, - aggs: aggConfigs, - filters: get(input, 'filters', undefined), - indexPattern, - inspectorAdapters, - partialRows: args.partialRows, - query: get(input, 'query', undefined) as any, - searchSessionId: getSearchSessionId(), - searchSourceService: searchSource, - timeFields: args.timeFields, - timeRange: get(input, 'timeRange', undefined), - }); + return { aggConfigs, indexPattern, searchSource }; + }).pipe( + switchMap(({ aggConfigs, indexPattern, searchSource }) => + handleEsaggsRequest({ + abortSignal, + aggs: aggConfigs, + filters: get(input, 'filters', undefined), + indexPattern, + inspectorAdapters, + partialRows: args.partialRows, + query: get(input, 'query', undefined) as any, + searchSessionId: getSearchSessionId(), + searchSourceService: searchSource, + timeFields: args.timeFields, + timeRange: get(input, 'timeRange', undefined), + }) + ) + ); }, }); } diff --git a/src/plugins/expression_reveal_image/common/constants.ts b/src/plugins/expression_reveal_image/common/constants.ts index 68ac53171ee7..2f5a38006832 100644 --- a/src/plugins/expression_reveal_image/common/constants.ts +++ b/src/plugins/expression_reveal_image/common/constants.ts @@ -7,3 +7,6 @@ */ export const PLUGIN_ID = 'expressionRevealImage'; export const PLUGIN_NAME = 'expressionRevealImage'; + +export const BASE64 = '`base64`'; +export const URL = 'URL'; diff --git a/src/plugins/expression_reveal_image/common/expression_functions/reveal_image.test.ts b/src/plugins/expression_reveal_image/common/expression_functions/reveal_image.test.ts index 633a132fea5e..999fd31980c6 100644 --- a/src/plugins/expression_reveal_image/common/expression_functions/reveal_image.test.ts +++ b/src/plugins/expression_reveal_image/common/expression_functions/reveal_image.test.ts @@ -8,21 +8,26 @@ import { functionWrapper, - elasticOutline, - elasticLogo, + getElasticOutline, + getElasticLogo, } from '../../../presentation_util/common/lib'; -import { getFunctionErrors } from '../i18n'; -import { revealImageFunction } from './reveal_image_function'; +import { revealImageFunction, errors } from './reveal_image_function'; import { Origin } from '../types'; import { ExecutionContext } from 'src/plugins/expressions'; -const errors = getFunctionErrors().revealImage; - describe('revealImageFunction', () => { const fn = functionWrapper(revealImageFunction); - it('returns a render as revealImage', () => { - const result = fn( + let elasticLogo = ''; + let elasticOutline = ''; + + beforeEach(async () => { + elasticLogo = (await getElasticLogo()).elasticLogo; + elasticOutline = (await getElasticOutline()).elasticOutline; + }); + + it('returns a render as revealImage', async () => { + const result = await fn( 0.5, { image: null, @@ -36,130 +41,147 @@ describe('revealImageFunction', () => { }); describe('context', () => { - it('throws when context is not a number between 0 and 1', () => { - expect(() => { - fn( - 10, - { - image: elasticLogo, - emptyImage: elasticOutline, - origin: Origin.TOP, - }, - {} as ExecutionContext - ); - }).toThrow(new RegExp(errors.invalidPercent(10).message)); + it('throws when context is not a number between 0 and 1', async () => { + expect.assertions(2); + await fn( + 10, + { + image: elasticLogo, + emptyImage: elasticOutline, + origin: Origin.TOP, + }, + {} as ExecutionContext + ).catch((e: any) => { + expect(e.message).toMatch(new RegExp(errors.invalidPercent(10).message)); + }); - expect(() => { - fn( - -0.1, - { - image: elasticLogo, - emptyImage: elasticOutline, - origin: Origin.TOP, - }, - {} as ExecutionContext - ); - }).toThrow(new RegExp(errors.invalidPercent(-0.1).message)); + await fn( + -0.1, + { + image: elasticLogo, + emptyImage: elasticOutline, + origin: Origin.TOP, + }, + {} as ExecutionContext + ).catch((e: any) => { + expect(e.message).toMatch(new RegExp(errors.invalidPercent(-0.1).message)); + }); }); }); describe('args', () => { describe('image', () => { - it('sets the image', () => { - const result = fn( - 0.89, - { - emptyImage: null, - origin: Origin.TOP, - image: elasticLogo, - }, - {} as ExecutionContext + it('sets the image', async () => { + const result = ( + await fn( + 0.89, + { + emptyImage: null, + origin: Origin.TOP, + image: elasticLogo, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('image', elasticLogo); }); - it('defaults to the Elastic outline logo', () => { - const result = fn( - 0.89, - { - emptyImage: null, - origin: Origin.TOP, - image: null, - }, - {} as ExecutionContext + it('defaults to the Elastic outline logo', async () => { + const result = ( + await fn( + 0.89, + { + emptyImage: null, + origin: Origin.TOP, + image: null, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('image', elasticOutline); }); }); describe('emptyImage', () => { - it('sets the background image', () => { - const result = fn( - 0, - { - emptyImage: elasticLogo, - origin: Origin.TOP, - image: null, - }, - {} as ExecutionContext + it('sets the background image', async () => { + const result = ( + await fn( + 0, + { + emptyImage: elasticLogo, + origin: Origin.TOP, + image: null, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('emptyImage', elasticLogo); }); - it('sets emptyImage to null', () => { - const result = fn( - 0, - { - emptyImage: null, - origin: Origin.TOP, - image: null, - }, - {} as ExecutionContext + it('sets emptyImage to null', async () => { + const result = ( + await fn( + 0, + { + emptyImage: null, + origin: Origin.TOP, + image: null, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('emptyImage', null); }); }); describe('origin', () => { - it('sets which side to start the reveal from', () => { - let result = fn( - 1, - { - emptyImage: null, - origin: Origin.TOP, - image: null, - }, - {} as ExecutionContext + it('sets which side to start the reveal from', async () => { + let result = ( + await fn( + 1, + { + emptyImage: null, + origin: Origin.TOP, + image: null, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('origin', 'top'); - result = fn( - 1, - { - emptyImage: null, - origin: Origin.LEFT, - image: null, - }, - {} as ExecutionContext + result = ( + await fn( + 1, + { + emptyImage: null, + origin: Origin.LEFT, + image: null, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('origin', 'left'); - result = fn( - 1, - { - emptyImage: null, - origin: Origin.BOTTOM, - image: null, - }, - {} as ExecutionContext + result = ( + await fn( + 1, + { + emptyImage: null, + origin: Origin.BOTTOM, + image: null, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('origin', 'bottom'); - result = fn( - 1, - { - emptyImage: null, - origin: Origin.RIGHT, - image: null, - }, - {} as ExecutionContext + result = ( + await fn( + 1, + { + emptyImage: null, + origin: Origin.RIGHT, + image: null, + }, + {} as ExecutionContext + ) ).value; expect(result).toHaveProperty('origin', 'right'); }); diff --git a/src/plugins/expression_reveal_image/common/expression_functions/reveal_image_function.ts b/src/plugins/expression_reveal_image/common/expression_functions/reveal_image_function.ts index 33e61e85f953..7056bb10518e 100644 --- a/src/plugins/expression_reveal_image/common/expression_functions/reveal_image_function.ts +++ b/src/plugins/expression_reveal_image/common/expression_functions/reveal_image_function.ts @@ -6,13 +6,77 @@ * Side Public License, v 1. */ -import { resolveWithMissingImage, elasticOutline } from '../../../presentation_util/common/lib'; -import { getFunctionHelp, getFunctionErrors } from '../i18n'; -import { ExpressionRevealImageFunction, Origin } from '../types'; +import { i18n } from '@kbn/i18n'; +import { + resolveWithMissingImage, + getElasticOutline, + isValidUrl, +} from '../../../presentation_util/common/lib'; +import { ExpressionRevealImageFunction, Origin, Position } from '../../common/types'; +import { BASE64, URL } from '../../common/constants'; + +const strings = { + help: i18n.translate('expressionRevealImage.functions.revealImageHelpText', { + defaultMessage: 'Configures an image reveal element.', + }), + args: { + image: i18n.translate('expressionRevealImage.functions.revealImage.args.imageHelpText', { + defaultMessage: + 'The image to reveal. Provide an image asset as a {BASE64} data {URL}, ' + + 'or pass in a sub-expression.', + values: { + BASE64, + URL, + }, + }), + emptyImage: i18n.translate( + 'expressionRevealImage.functions.revealImage.args.emptyImageHelpText', + { + defaultMessage: + 'An optional background image to reveal over. ' + + 'Provide an image asset as a `{BASE64}` data {URL}, or pass in a sub-expression.', + values: { + BASE64, + URL, + }, + } + ), + origin: i18n.translate('expressionRevealImage.functions.revealImage.args.originHelpText', { + defaultMessage: 'The position to start the image fill. For example, {list}, or {end}.', + values: { + list: Object.values(Position) + .slice(0, -1) + .map((position) => `\`"${position}"\``) + .join(', '), + end: Object.values(Position).slice(-1)[0], + }, + }), + }, +}; + +export const errors = { + invalidPercent: (percent: number) => + new Error( + i18n.translate('expressionRevealImage.functions.revealImage.invalidPercentErrorMessage', { + defaultMessage: "Invalid value: '{percent}'. Percentage must be between 0 and 1", + values: { + percent, + }, + }) + ), + invalidImageUrl: (imageUrl: string) => + new Error( + i18n.translate('expressionRevealImage.functions.revealImage.invalidImageUrl', { + defaultMessage: "Invalid image url: '{imageUrl}'.", + values: { + imageUrl, + }, + }) + ), +}; export const revealImageFunction: ExpressionRevealImageFunction = () => { - const { help, args: argHelp } = getFunctionHelp().revealImage; - const errors = getFunctionErrors().revealImage; + const { help, args: argHelp } = strings; return { name: 'revealImage', @@ -24,7 +88,7 @@ export const revealImageFunction: ExpressionRevealImageFunction = () => { image: { types: ['string', 'null'], help: argHelp.image, - default: elasticOutline, + default: null, }, emptyImage: { types: ['string', 'null'], @@ -38,11 +102,16 @@ export const revealImageFunction: ExpressionRevealImageFunction = () => { options: Object.values(Origin), }, }, - fn: (percent, args) => { + fn: async (percent, args) => { if (percent > 1 || percent < 0) { throw errors.invalidPercent(percent); } + if (args.image && !isValidUrl(args.image)) { + throw errors.invalidImageUrl(args.image); + } + + const { elasticOutline } = await getElasticOutline(); return { type: 'render', as: 'revealImage', diff --git a/src/plugins/expression_reveal_image/common/i18n/constants.ts b/src/plugins/expression_reveal_image/common/i18n/constants.ts deleted file mode 100644 index 413f376515a3..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/constants.ts +++ /dev/null @@ -1,10 +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 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 BASE64 = '`base64`'; -export const URL = 'URL'; diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_functions/dict/reveal_image.ts b/src/plugins/expression_reveal_image/common/i18n/expression_functions/dict/reveal_image.ts deleted file mode 100644 index ccf9967bd6a6..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/dict/reveal_image.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { i18n } from '@kbn/i18n'; -import { Position } from '../../../types'; -import { BASE64, URL } from '../../constants'; - -export const help = { - help: i18n.translate('expressionRevealImage.functions.revealImageHelpText', { - defaultMessage: 'Configures an image reveal element.', - }), - args: { - image: i18n.translate('expressionRevealImage.functions.revealImage.args.imageHelpText', { - defaultMessage: - 'The image to reveal. Provide an image asset as a {BASE64} data {URL}, ' + - 'or pass in a sub-expression.', - values: { - BASE64, - URL, - }, - }), - emptyImage: i18n.translate( - 'expressionRevealImage.functions.revealImage.args.emptyImageHelpText', - { - defaultMessage: - 'An optional background image to reveal over. ' + - 'Provide an image asset as a `{BASE64}` data {URL}, or pass in a sub-expression.', - values: { - BASE64, - URL, - }, - } - ), - origin: i18n.translate('expressionRevealImage.functions.revealImage.args.originHelpText', { - defaultMessage: 'The position to start the image fill. For example, {list}, or {end}.', - values: { - list: Object.values(Position) - .slice(0, -1) - .map((position) => `\`"${position}"\``) - .join(', '), - end: Object.values(Position).slice(-1)[0], - }, - }), - }, -}; -export const errors = { - invalidPercent: (percent: number) => - new Error( - i18n.translate('expressionRevealImage.functions.revealImage.invalidPercentErrorMessage', { - defaultMessage: "Invalid value: '{percent}'. Percentage must be between 0 and 1", - values: { - percent, - }, - }) - ), -}; diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_help.ts b/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_help.ts deleted file mode 100644 index 30e79b120771..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_help.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { help as revealImage } from './dict/reveal_image'; - -/** - * Help text for Canvas Functions should be properly localized. This function will - * return a dictionary of help strings, organized by `ExpressionFunctionDefinition` - * specification and then by available arguments within each `ExpressionFunctionDefinition`. - * - * This a function, rather than an object, to future-proof string initialization, - * if ever necessary. - */ -export const getFunctionHelp = () => ({ - revealImage, -}); diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_functions/index.ts b/src/plugins/expression_reveal_image/common/i18n/expression_functions/index.ts deleted file mode 100644 index 3d36b123421f..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/index.ts +++ /dev/null @@ -1,10 +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 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 * from './function_help'; -export * from './function_errors'; diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts b/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts deleted file mode 100644 index 4f70f9d30b74..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/index.ts +++ /dev/null @@ -1,9 +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 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 { strings as revealImage } from './reveal_image'; diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/reveal_image.ts b/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/reveal_image.ts deleted file mode 100644 index a32fdbd4c0b5..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/dict/reveal_image.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { i18n } from '@kbn/i18n'; - -export const strings = { - getDisplayName: () => - i18n.translate('expressionRevealImage.renderer.revealImage.displayName', { - defaultMessage: 'Image reveal', - }), - getHelpDescription: () => - i18n.translate('expressionRevealImage.renderer.revealImage.helpDescription', { - defaultMessage: 'Reveal a percentage of an image to make a custom gauge-style chart', - }), -}; diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/index.ts b/src/plugins/expression_reveal_image/common/i18n/expression_renderers/index.ts deleted file mode 100644 index 7e637f240d15..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/index.ts +++ /dev/null @@ -1,9 +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 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 * from './renderer_strings'; diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/renderer_strings.ts b/src/plugins/expression_reveal_image/common/i18n/expression_renderers/renderer_strings.ts deleted file mode 100644 index b74230a2a5d7..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/expression_renderers/renderer_strings.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { revealImage } from './dict'; - -/** - * Help text for Canvas Functions should be properly localized. This function will - * return a dictionary of help strings, organized by `ExpressionFunctionDefinition` - * specification and then by available arguments within each `ExpressionFunctionDefinition`. - * - * This a function, rather than an object, to future-proof string initialization, - * if ever necessary. - */ -export const getRendererStrings = () => ({ - revealImage, -}); diff --git a/src/plugins/expression_reveal_image/common/i18n/index.ts b/src/plugins/expression_reveal_image/common/i18n/index.ts deleted file mode 100644 index 9c50bfab1305..000000000000 --- a/src/plugins/expression_reveal_image/common/i18n/index.ts +++ /dev/null @@ -1,10 +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 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 * from './expression_functions'; -export * from './expression_renderers'; diff --git a/src/plugins/expression_reveal_image/common/types/expression_functions.ts b/src/plugins/expression_reveal_image/common/types/expression_functions.ts index ee291e204acf..2d836bf8ad99 100644 --- a/src/plugins/expression_reveal_image/common/types/expression_functions.ts +++ b/src/plugins/expression_reveal_image/common/types/expression_functions.ts @@ -31,7 +31,7 @@ export type ExpressionRevealImageFunction = () => ExpressionFunctionDefinition< 'revealImage', number, Arguments, - ExpressionValueRender + Promise> >; export enum Position { diff --git a/src/plugins/expression_reveal_image/common/types/expression_renderers.ts b/src/plugins/expression_reveal_image/common/types/expression_renderers.ts index 77dacaefc1bd..b04c292433f9 100644 --- a/src/plugins/expression_reveal_image/common/types/expression_renderers.ts +++ b/src/plugins/expression_reveal_image/common/types/expression_renderers.ts @@ -11,7 +11,7 @@ export type OriginString = 'bottom' | 'left' | 'top' | 'right'; export interface RevealImageRendererConfig { percent: number; origin?: OriginString; - image?: string; + image: string; emptyImage?: string; } diff --git a/src/plugins/expression_reveal_image/public/components/reveal_image.scss b/src/plugins/expression_reveal_image/public/components/reveal_image.scss deleted file mode 100644 index f94668b7cdfa..000000000000 --- a/src/plugins/expression_reveal_image/public/components/reveal_image.scss +++ /dev/null @@ -1,18 +0,0 @@ -.revealImage { - height: 100%; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - pointer-events: none; - - .revealImageAligner { - background-size: contain; - background-repeat: no-repeat; - } - - // disables selection and dragging - .revealImage__image { - user-select: none; - } -} diff --git a/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx b/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx index a9c24fca78d9..d20bbdc1bf19 100644 --- a/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx +++ b/src/plugins/expression_reveal_image/public/components/reveal_image_component.tsx @@ -9,9 +9,27 @@ import React, { useRef, useState, useEffect, useCallback } from 'react'; import { useResizeObserver } from '@elastic/eui'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { css, CSSObject } from '@emotion/react'; import { NodeDimensions, RevealImageRendererConfig, OriginString } from '../../common/types'; -import { isValidUrl, elasticOutline } from '../../../presentation_util/public'; -import './reveal_image.scss'; +import { isValidUrl } from '../../../presentation_util/public'; + +const revealImageParentStyle = css` + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +`; + +const revealImageAlignerStyle: CSSObject = { + backgroundSize: 'contain', + backgroundRepeat: 'no-repeat', +}; + +const revealImageStyle: CSSObject = { + userSelect: 'none', +}; interface RevealImageComponentProps extends RevealImageRendererConfig { onLoaded: IInterpreterRenderHandlers['done']; @@ -19,8 +37,8 @@ interface RevealImageComponentProps extends RevealImageRendererConfig { } interface ImageStyles { - width?: string; - height?: string; + width?: number | string; + height?: number | string; clipPath?: string; } @@ -48,6 +66,7 @@ function RevealImageComponent({ // modify the top-level container class parentNode.className = 'revealImage'; + parentNode.setAttribute('style', revealImageParentStyle.styles); // set up the overlay image const updateImageView = useCallback(() => { @@ -89,43 +108,46 @@ function RevealImageComponent({ }; if (imgDimensions.ratio > domNodeDimensions.ratio) { - imgStyles.height = `${domNodeDimensions.height}px`; + imgStyles.height = domNodeDimensions.height; imgStyles.width = 'initial'; } else { - imgStyles.width = `${domNodeDimensions.width}px`; + imgStyles.width = domNodeDimensions.width; imgStyles.height = 'initial'; } return imgStyles; } - const imgSrc = isValidUrl(image ?? '') ? image : elasticOutline; - - const alignerStyles: AlignerStyles = {}; + const additionaAlignerStyles: AlignerStyles = {}; if (isValidUrl(emptyImage ?? '')) { // only use empty image if one is provided - alignerStyles.backgroundImage = `url(${emptyImage})`; + additionaAlignerStyles.backgroundImage = `url(${emptyImage})`; } - let imgStyles: ImageStyles = {}; - if (imgRef.current && loaded) imgStyles = getImageSizeStyle(); + let additionalImgStyles: ImageStyles = {}; + if (imgRef.current && loaded) additionalImgStyles = getImageSizeStyle(); - imgStyles.clipPath = getClipPath(percent, origin); + additionalImgStyles.clipPath = getClipPath(percent, origin); if (imgRef.current && loaded) { imgRef.current.style.setProperty('-webkit-clip-path', getClipPath(percent, origin)); } return ( -
+
); diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx index bc70b3685e24..863d8d1000f3 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx @@ -9,18 +9,30 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { revealImageRenderer } from '../'; -import { elasticOutline, elasticLogo } from '../../../../presentation_util/public'; -import { Render } from '../../../../presentation_util/public/__stories__'; - +import { getElasticOutline, getElasticLogo } from '../../../../presentation_util/public'; +import { Render, waitFor } from '../../../../presentation_util/public/__stories__'; import { Origin } from '../../../common/types/expression_functions'; -storiesOf('renderers/revealImage', module).add('default', () => { +const Renderer = ({ + elasticLogo, + elasticOutline, +}: { + elasticLogo: string; + elasticOutline: string; +}) => { const config = { image: elasticLogo, emptyImage: elasticOutline, origin: Origin.LEFT, percent: 0.45, }; - return ; -}); +}; + +storiesOf('renderers/revealImage', module).add( + 'default', + (_, props) => ( + + ), + { decorators: [waitFor(getElasticLogo()), waitFor(getElasticOutline())] } +); diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx index 4d84de3da994..c89272ba58ad 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx @@ -9,21 +9,30 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { i18n } from '@kbn/i18n'; import { withSuspense } from '../../../presentation_util/public'; -import { getRendererStrings } from '../../common/i18n'; import { RevealImageRendererConfig } from '../../common/types'; -const { revealImage: revealImageStrings } = getRendererStrings(); +export const strings = { + getDisplayName: () => + i18n.translate('expressionRevealImage.renderer.revealImage.displayName', { + defaultMessage: 'Image reveal', + }), + getHelpDescription: () => + i18n.translate('expressionRevealImage.renderer.revealImage.helpDescription', { + defaultMessage: 'Reveal a percentage of an image to make a custom gauge-style chart', + }), +}; const LazyRevealImageComponent = lazy(() => import('../components/reveal_image_component')); const RevealImageComponent = withSuspense(LazyRevealImageComponent, null); export const revealImageRenderer = (): ExpressionRenderDefinition => ({ name: 'revealImage', - displayName: revealImageStrings.getDisplayName(), - help: revealImageStrings.getHelpDescription(), + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), reuseDomNode: true, - render: async ( + render: ( domNode: HTMLElement, config: RevealImageRendererConfig, handlers: IInterpreterRenderHandlers @@ -34,7 +43,7 @@ export const revealImageRenderer = (): ExpressionRenderDefinition - + , domNode ); diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index 86477e53dc1a..4c0ed842076c 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -8,6 +8,7 @@ import { of } from 'rxjs'; import { first, skip, toArray } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; import { loader, ExpressionLoader } from './loader'; import { Observable } from 'rxjs'; import { @@ -22,6 +23,8 @@ const { __getLastExecution, __getLastRenderMode } = require('./services'); const element: HTMLElement = null as any; +let testScheduler: TestScheduler; + jest.mock('./services', () => { let renderMode: RenderMode | undefined; const renderers: Record = { @@ -88,6 +91,10 @@ describe('execute helper function', () => { describe('ExpressionLoader', () => { const expressionString = 'demodata'; + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); + }); + describe('constructor', () => { it('accepts expression string', () => { const expressionLoader = new ExpressionLoader(element, expressionString, {}); @@ -130,6 +137,7 @@ describe('ExpressionLoader', () => { const expressionLoader = new ExpressionLoader(element, 'var foo', { variables: { foo: of(1, 2) }, partial: true, + throttle: 0, }); const { result, partial } = await expressionLoader.data$.pipe(first()).toPromise(); @@ -137,6 +145,22 @@ describe('ExpressionLoader', () => { expect(result).toBe(1); }); + it('throttles partial results', async () => { + testScheduler.run(({ cold, expectObservable }) => { + const expressionLoader = new ExpressionLoader(element, 'var foo', { + variables: { foo: cold('a 5ms b 5ms c 10ms d', { a: 1, b: 2, c: 3, d: 4 }) }, + partial: true, + throttle: 20, + }); + + expectObservable(expressionLoader.data$).toBe('a 19ms c 2ms d', { + a: expect.objectContaining({ result: 1 }), + c: expect.objectContaining({ result: 3 }), + d: expect.objectContaining({ result: 4 }), + }); + }); + }); + it('emits on loading$ on initial load and on updates', async () => { const expressionLoader = new ExpressionLoader(element, expressionString, {}); const loadingPromise = expressionLoader.loading$.pipe(toArray()).toPromise(); diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index a51ce35c6818..e5e63b044ad0 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; -import { filter, map, delay } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject, Subscription, asyncScheduler, identity } from 'rxjs'; +import { filter, map, delay, throttleTime } from 'rxjs/operators'; import { defaults } from 'lodash'; import { UnwrapObservable } from '@kbn/utility-types'; import { Adapters } from '../../inspector/public'; @@ -145,7 +145,10 @@ export class ExpressionLoader { .getData() .pipe( delay(0), // delaying until the next tick since we execute the expression in the constructor - filter(({ partial }) => params.partial || !partial) + filter(({ partial }) => params.partial || !partial), + params.partial && params.throttle + ? throttleTime(params.throttle, asyncScheduler, { leading: true, trailing: true }) + : identity ) .subscribe((value) => this.dataSubject.next(value)); }; @@ -178,6 +181,7 @@ export class ExpressionLoader { this.params.syncColors = params.syncColors; this.params.debug = Boolean(params.debug); this.params.partial = Boolean(params.partial); + this.params.throttle = Number(params.throttle ?? 1000); this.params.inspectorAdapters = (params.inspectorAdapters || this.execution?.inspect()) as Adapters; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 55655cfc5d15..3aa902be5ba6 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -908,7 +908,6 @@ export interface IExpressionLoaderParams { // // (undocumented) onRenderError?: RenderErrorHandlerFnType; - // (undocumented) partial?: boolean; // Warning: (ae-forgotten-export) The symbol "RenderMode" needs to be exported by the entry point index.d.ts // @@ -920,6 +919,7 @@ export interface IExpressionLoaderParams { searchSessionId?: string; // (undocumented) syncColors?: boolean; + throttle?: number; // (undocumented) uiState?: unknown; // (undocumented) diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 2375252e8278..a691aa31a75c 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -48,7 +48,18 @@ export interface IExpressionLoaderParams { renderMode?: RenderMode; syncColors?: boolean; hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; + + /** + * The flag to toggle on emitting partial results. + * By default, the partial results are disabled. + */ partial?: boolean; + + /** + * Throttling of partial results in milliseconds. 0 is disabling the throttling. + * By default, it equals 1000. + */ + throttle?: number; } export interface ExpressionRenderError extends Error { diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 816322dbe529..6eb5c9fe38ba 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -7,62 +7,19 @@ */ /* eslint max-len: 0 */ -/* eslint-disable */ import { i18n } from '@kbn/i18n'; import { SavedObject } from 'kibana/server'; export const getSavedObjects = (): SavedObject[] => [ - { - id: 'aeb212e0-4c84-11e8-b3d7-01146121b73d', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.controlsTitle', { - defaultMessage: '[Flights] Controls', - }), - visState: - '{"title":"[Flights] Controls","type":"input_control_vis","params":{"controls":[{"id":"1525098134264","indexPattern":"d3d7af60-4c81-11e8-b3d7-01146121b73d","fieldName":"OriginCityName","parent":"","label":"Origin City","type":"list","options":{"type":"terms","multiselect":true,"size":100,"order":"desc"}},{"id":"1525099277699","indexPattern":"d3d7af60-4c81-11e8-b3d7-01146121b73d","fieldName":"DestCityName","parent":"1525098134264","label":"Destination City","type":"list","options":{"type":"terms","multiselect":true,"size":100,"order":"desc"}},{"id":"1525099307278","indexPattern":"d3d7af60-4c81-11e8-b3d7-01146121b73d","fieldName":"AvgTicketPrice","parent":"","label":"Average Ticket Price","type":"range","options":{"decimalPlaces":0,"step":10}}],"updateFiltersOnChange":false,"useTimeFilter":true,"pinFilters":false},"aggs":[]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - references: [], - }, - { - id: 'c8fc3d30-4c87-11e8-b3d7-01146121b73d', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.flightCountAndAverageTicketPriceTitle', { - defaultMessage: '[Flights] Flight Count and Average Ticket Price', - }), - visState: - '{"title":"[Flights] Flight Count and Average Ticket Price","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Average Ticket Price"}},{"id":"ValueAxis-2","name":"RightAxis-1","type":"value","position":"right","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Flight Count"}}],"seriesParams":[{"show":true,"mode":"stacked","type":"area","drawLinesBetweenPoints":true,"showCircles":false,"interpolate":"linear","lineWidth":2,"data":{"id":"5","label":"Flight Count"},"valueAxis":"ValueAxis-2"},{"show":true,"mode":"stacked","type":"line","drawLinesBetweenPoints":false,"showCircles":true,"interpolate":"linear","data":{"id":"4","label":"Average Ticket Price"},"valueAxis":"ValueAxis-1","lineWidth":2}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":13,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"5","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Flight Count"}},{"id":"4","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice","customLabel":"Average Ticket Price"}},{"id":"2","enabled":true,"type":"avg","schema":"radius","params":{"field":"AvgTicketPrice"}}]}', - uiStateJSON: - '{"vis":{"legendOpen":true,"colors":{"Average Ticket Price":"#629E51","Flight Count":"#AEA2E0"}}}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, { id: '571aaf70-4c88-11e8-b3d7-01146121b73d', type: 'search', - updated_at: '2018-05-09T15:49:03.736Z', + updated_at: '2021-07-01T20:41:40.379Z', version: '1', - migrationVersion: {}, + migrationVersion: { + search: '7.9.3', + }, attributes: { title: i18n.translate('home.sampleData.flightsSpec.flightLogTitle', { defaultMessage: '[Flights] Flight Log', @@ -84,67 +41,31 @@ export const getSavedObjects = (): SavedObject[] => [ version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","highlightAll":true,"version":true,"query":{"language":"kuery","query":""},"filter":[]}', + '{"highlightAll":true,"version":true,"query":{"language":"kuery","query":""},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', }, }, - references: [], - }, - { - id: '8f4d0c00-4c86-11e8-b3d7-01146121b73d', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.airlineCarrierTitle', { - defaultMessage: '[Flights] Airline Carrier', - }), - visState: - '{"title":"[Flights] Airline Carrier","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100},"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Carrier","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":false}}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - { - id: 'f8290060-4c88-11e8-b3d7-01146121b73d', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.delayTypeTitle', { - defaultMessage: '[Flights] Delay Type', - }), - visState: - '{"title":"[Flights] Delay Type","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"FlightDelayType","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', + references: [ + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', }, - }, - references: [], + ], }, { id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', type: 'visualization', updated_at: '2018-05-09T15:49:03.736Z', version: '1', - migrationVersion: {}, + migrationVersion: { + visualization: '7.14.0', + }, attributes: { title: i18n.translate('home.sampleData.flightsSpec.delaysAndCancellationsTitle', { defaultMessage: '[Flights] Delays & Cancellations', }), visState: - '{"title":"[Flights] Delays & Cancellations","type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"filter_ratio","numerator":"FlightDelay:true"}],"separate_axis":0,"axis_position":"right","formatter":"percent","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none","label":"Percent Delays"}],"time_field":"timestamp","index_pattern_ref_name":"ref_1_index_pattern","interval":">=1h","axis_position":"left","axis_formatter":"number","show_legend":1,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier","template":"{{Carrier}}: Flight Delayed and Cancelled!","index_pattern_ref_name":"ref_2_index_pattern","query_string":"FlightDelay:true AND Cancelled:true","id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39","color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle","ignore_global_filters":1,"ignore_panel_filters":1}],"legend_position":"bottom","use_kibana_indexes":true},"aggs":[]}', + '{"title":"[Flights] Delays & Cancellations","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"filter_ratio","numerator":{"query":"FlightDelay:true","language":"lucene"}}],"separate_axis":0,"axis_position":"right","formatter":"percent","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none","label":"Percent Delays","split_color_mode":"gradient"}],"time_field":"timestamp","interval":">=1h","axis_position":"left","axis_formatter":"number","show_legend":0,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier","template":"{{Carrier}}: Flight Delayed and Cancelled!","query_string":{"query":"FlightDelay:true AND Cancelled:true","language":"lucene"},"id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39","color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle","ignore_global_filters":1,"ignore_panel_filters":1,"index_pattern_ref_name":"metrics_1_index_pattern"}],"legend_position":"bottom","use_kibana_indexes":true,"axis_scale":"normal","tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"axis_max":"1","index_pattern_ref_name":"metrics_0_index_pattern"}}', uiStateJSON: '{}', description: '', version: 1, @@ -154,87 +75,56 @@ export const getSavedObjects = (): SavedObject[] => [ }, references: [ { - name: 'ref_1_index_pattern', - type: 'index_pattern', - id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d' + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'metrics_0_index_pattern', + type: 'index-pattern', }, { - name: 'ref_2_index_pattern', - type: 'index_pattern', - id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d' - } - ] + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'metrics_1_index_pattern', + type: 'index-pattern', + }, + ], }, { id: '9886b410-4c8b-11e8-b3d7-01146121b73d', type: 'visualization', updated_at: '2018-05-09T15:49:03.736Z', version: '1', - migrationVersion: {}, + migrationVersion: { + visualization: '7.14.0', + }, attributes: { title: i18n.translate('home.sampleData.flightsSpec.delayBucketsTitle', { defaultMessage: '[Flights] Delay Buckets', }), visState: - '{"title":"[Flights] Delay Buckets","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"histogram","schema":"segment","params":{"field":"FlightDelayMin","interval":30,"extended_bounds":{},"customLabel":"Flight Delay Minutes"}}]}', + '{"title":"[Flights] Delay Buckets","type":"histogram","aggs":[{"id":"1","enabled":true,"type":"count","params":{},"schema":"metric"},{"id":"2","enabled":true,"type":"histogram","params":{"field":"FlightDelayMin","interval":30,"used_interval":30,"min_doc_count":false,"has_extended_bounds":false,"extended_bounds":{},"customLabel":"Flight Delay Minutes"},"schema":"segment"}],"params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"},"isVislibVis":true,"radiusRatio":0,"labels":{"show":false},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#E7664C"}}}', uiStateJSON: '{"vis":{"legendOpen":false}}', description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[{"meta":{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","negate":true,"disabled":false,"alias":null,"type":"phrase","key":"FlightDelayMin","value":"0","params":{"query":0,"type":"phrase"}},"query":{"match":{"FlightDelayMin":{"query":0,"type":"phrase"}}},"$state":{"store":"appState"}}],"query":{"query":"","language":"kuery"}}', + '{"query":{"query":"","language":"kuery"},"filter":[{"meta":{"negate":true,"disabled":false,"alias":null,"type":"phrase","key":"FlightDelayMin","value":"0","params":{"query":0,"type":"phrase"},"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"},"query":{"match":{"FlightDelayMin":{"query":0,"type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', }, }, - references: [], - }, - { - id: '76e3c090-4c8c-11e8-b3d7-01146121b73d', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.flightDelaysTitle', { - defaultMessage: '[Flights] Flight Delays', - }), - visState: - '{"title":"[Flights] Flight Delays","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"FlightDelay","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Delays"}}]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', + references: [ + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', }, - }, - references: [], - }, - { - id: '707665a0-4c8c-11e8-b3d7-01146121b73d', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.flightCancellationsTitle', { - defaultMessage: '[Flights] Flight Cancellations', - }), - visState: - '{"title":"[Flights] Flight Cancellations","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"filter":true},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"BottomAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"detailedTooltip":true,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Cancelled","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Flight Cancellations"}}]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', }, - }, - references: [], + ], }, { id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', + updated_at: '2021-07-07T01:48:55.366Z', version: '1', migrationVersion: {}, attributes: { @@ -242,34 +132,40 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Destination Weather', }), visState: - '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Flights] Destination Weather","type":"tagcloud","aggs":[{"id":"1","enabled":true,"type":"count","params":{},"schema":"metric"},{"id":"2","enabled":true,"type":"terms","params":{"field":"DestWeather","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"},"schema":"segment"}],"params":{"scale":"linear","orientation":"single","minFontSize":12,"maxFontSize":46,"showLabel":false,"palette":{"type":"palette","name":"temperature"}}}', uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', + '{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', }, }, - references: [], + references: [ + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + }, + ], }, { - id: '129be430-4c93-11e8-b3d7-01146121b73d', + id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', + updated_at: '2021-07-07T01:36:42.568Z', + version: '4', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.flightsSpec.markdownInstructionsTitle', { - defaultMessage: '[Flights] Markdown Instructions', + title: i18n.translate('home.sampleData.flightsSpec.airportConnectionsTitle', { + defaultMessage: '[Flights] Airport Connections (Hover Over Airport)', }), visState: - '{"title":"[Flights] Markdown Instructions","type":"markdown","params":{"fontSize":10,"openLinksInNewTab":true,"markdown":"### Sample Flight data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"aggs":[]}', + '{"title":"[Flights] Airport Connections (Hover Over Airport)","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -70, zoom: 2}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\"OriginAirportID\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"OriginLocation\\", \\"Origin\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\"DestAirportID\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"DestLocation\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.origins.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\"!selected\\", remove: true}\\n {trigger: \\"selected\\", insert: \\"selected\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\"@airport:mouseover\\", update: \\"datum\\"}\\n {events: \\"@airport:mouseout\\", update: \\"null\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n {signal: \\"zoom*zoom*2+1\\"}\\n {signal: \\"zoom*zoom*20+1\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\"formula\\", expr: \\"{x:parent.x, y:parent.y}\\", as: \\"source\\"}\\n {type: \\"formula\\", expr: \\"{x:datum.x, y:datum.y}\\", as: \\"target\\"}\\n {type: \\"linkpath\\", shape: \\"diagonal\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\"facetDatumElems\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\"path\\"}\\n stroke: {value: \\"black\\"}\\n strokeWidth: {scale: \\"lineThickness\\", field: \\"doc_count\\"}\\n strokeOpacity: {scale: \\"lineOpacity\\", field: \\"doc_count\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"airportSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{title: datum.originLocation.hits.hits[0]._source.Origin + \' (\' + datum.key + \')\', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"}}', uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: '{}', + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', }, }, references: [], @@ -279,155 +175,30 @@ export const getSavedObjects = (): SavedObject[] => [ type: 'visualization', updated_at: '2018-05-09T15:49:03.736Z', version: '1', - migrationVersion: {}, + migrationVersion: { + visualization: '7.14.0', + }, attributes: { title: i18n.translate('home.sampleData.flightsSpec.departuresCountMapTitle', { defaultMessage: '[Flights] Departures Count Map', }), - visState: '{\"title\":\"[Flights] Departure Count Map\",\"type\":\"vega\",\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\\"OriginLocation\\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\\"OriginLocation\\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.gridSplit.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"gridSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"}}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - { - id: 'f8283bf0-52fd-11e8-a160-89cc2ad9e8e2', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.totalFlightDelaysTitle', { - defaultMessage: '[Flights] Total Flight Delays', - }), visState: - '{"title":"[Flights] Total Flight Delays","type":"gauge","params":{"type":"gauge","addTooltip":true,"addLegend":true,"isDisplayWarning":false,"gauge":{"verticalSplit":false,"extendRange":true,"percentageMode":false,"gaugeType":"Arc","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Blues","gaugeColorMode":"Labels","colorsRange":[{"from":0,"to":75},{"from":75,"to":150},{"from":150,"to":225},{"from":225,"to":300}],"invertColors":true,"labels":{"show":false,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333"},"type":"meter","style":{"bgWidth":0.9,"width":0.9,"mask":false,"bgMask":false,"maskBars":50,"bgFill":"#eee","bgColor":false,"subText":"","fontSize":60,"labelColor":true}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total Delays"}}]}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 75":"rgb(8,48,107)","75 - 150":"rgb(55,135,192)","150 - 225":"rgb(171,208,230)","225 - 300":"rgb(247,251,255)"}}}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[{"meta":{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"FlightDelay","value":"true","params":{"query":true,"type":"phrase"}},"query":{"match":{"FlightDelay":{"query":true,"type":"phrase"}}},"$state":{"store":"appState"}}],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - { - id: '08884800-52fe-11e8-a160-89cc2ad9e8e2', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.totalFlightCancellationsTitle', { - defaultMessage: '[Flights] Total Flight Cancellations', - }), - visState: - '{"title":"[Flights] Total Flight Cancellations","type":"gauge","params":{"type":"gauge","addTooltip":true,"addLegend":true,"isDisplayWarning":false,"gauge":{"verticalSplit":false,"extendRange":true,"percentageMode":false,"gaugeType":"Arc","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Blues","gaugeColorMode":"Labels","colorsRange":[{"from":0,"to":75},{"from":75,"to":150},{"from":150,"to":225},{"from":225,"to":300}],"invertColors":true,"labels":{"show":false,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333"},"type":"meter","style":{"bgWidth":0.9,"width":0.9,"mask":false,"bgMask":false,"maskBars":50,"bgFill":"#eee","bgColor":false,"subText":"","fontSize":60,"labelColor":true}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total Cancellations"}}]}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 75":"rgb(8,48,107)","75 - 150":"rgb(55,135,192)","150 - 225":"rgb(171,208,230)","225 - 300":"rgb(247,251,255)"}}}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[{"meta":{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"Cancelled","value":"true","params":{"query":true,"type":"phrase"}},"query":{"match":{"Cancelled":{"query":true,"type":"phrase"}}},"$state":{"store":"appState"}}],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - { - id: 'e6944e50-52fe-11e8-a160-89cc2ad9e8e2', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.originCountryTitle', { - defaultMessage: '[Flights] Origin Country vs. Destination Country', - }), - visState: - '{"title":"[Flights] Origin Country vs. Destination Country","type":"heatmap","params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":false,"legendPosition":"right","times":[],"colorsNumber":5,"colorSchema":"Blues","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"overwriteColor":false,"color":"#555"}}]},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"group","params":{"field":"OriginCountry","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Origin Country"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestCountry","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Destination Country"}}]}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 22":"rgb(247,251,255)","22 - 44":"rgb(208,225,242)","44 - 66":"rgb(148,196,223)","66 - 88":"rgb(74,152,201)","88 - 110":"rgb(23,100,171)"}}}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - { - id: '01c413e0-5395-11e8-99bf-1ba7b1bdaa61', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.totalFlightsTitle', { - defaultMessage: '[Flights] Total Flights', - }), - visState: - '{"title":"[Flights] Total Flights","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":36}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total Flights"}}]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - { - id: '2edf78b0-5395-11e8-99bf-1ba7b1bdaa61', - type: 'visualization', - updated_at: '2018-05-09T15:49:03.736Z', - version: '1', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.averageTicketPriceTitle', { - defaultMessage: '[Flights] Average Ticket Price', - }), - visState: - '{"title":"[Flights] Average Ticket Price","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":36}}},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice","customLabel":"Avg. Ticket Price"}}]}', + '{"title":"[Flights] Departure Count Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"OriginLocation\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"OriginLocation\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"}}', uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filter":[],"query":{"query":"","language":"kuery"}}', + '{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', }, }, - references: [], - }, - { - id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', - type: 'visualization', - updated_at: '2018-05-09T15:55:51.195Z', - version: '3', - migrationVersion: {}, - attributes: { - title: i18n.translate('home.sampleData.flightsSpec.airportConnectionsTitle', { - defaultMessage: '[Flights] Airport Connections (Hover Over Airport)', - }), - visState: - '{"aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\"OriginAirportID\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"OriginLocation\\", \\"Origin\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\"DestAirportID\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\"DestLocation\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.origins.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\"!selected\\", remove: true}\\n {trigger: \\"selected\\", insert: \\"selected\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\"@airport:mouseover\\", update: \\"datum\\"}\\n {events: \\"@airport:mouseout\\", update: \\"null\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n {signal: \\"zoom*zoom*0.2+1\\"}\\n {signal: \\"zoom*zoom*10+1\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\"formula\\", expr: \\"{x:parent.x, y:parent.y}\\", as: \\"source\\"}\\n {type: \\"formula\\", expr: \\"{x:datum.x, y:datum.y}\\", as: \\"target\\"}\\n {type: \\"linkpath\\", shape: \\"diagonal\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\"facetDatumElems\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\"path\\"}\\n stroke: {value: \\"black\\"}\\n strokeWidth: {scale: \\"lineThickness\\", field: \\"doc_count\\"}\\n strokeOpacity: {scale: \\"lineOpacity\\", field: \\"doc_count\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"airportSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{title: datum.originLocation.hits.hits[0]._source.Origin + \' (\' + datum.key + \')\', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"},"title":"[Flights] Airport Connections (Hover Over Airport)","type":"vega"}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + references: [ + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', }, - }, - references: [], + ], }, { id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', @@ -440,7 +211,7 @@ export const getSavedObjects = (): SavedObject[] => [ timeFieldName: 'timestamp', fieldFormatMap: '{"hour_of_day":{"id":"number","params":{"pattern":"00"}},"AvgTicketPrice":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', - runtimeFieldMap: + runtimeFieldMap: '{"hour_of_day":{"type":"long","script":{"source":"emit(doc[\'timestamp\'].value.hourOfDay);"}}}', }, references: [], @@ -448,127 +219,194 @@ export const getSavedObjects = (): SavedObject[] => [ { id: '7adfa750-4c81-11e8-b3d7-01146121b73d', type: 'dashboard', - updated_at: '2018-05-09T15:59:04.578Z', - version: '4', + updated_at: '2021-07-07T14:16:23.001Z', + version: '5', references: [ { - name: 'panel_0', - type: 'visualization', - id: 'aeb212e0-4c84-11e8-b3d7-01146121b73d', + id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + name: '4:panel_4', + type: 'search', }, { - name: 'panel_1', + id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', + name: '7:panel_7', type: 'visualization', - id: 'c8fc3d30-4c87-11e8-b3d7-01146121b73d', }, { - name: 'panel_2', - type: 'search', - id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + id: '9886b410-4c8b-11e8-b3d7-01146121b73d', + name: '10:panel_10', + type: 'visualization', }, { - name: 'panel_3', + id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', + name: '21:panel_21', type: 'visualization', - id: '8f4d0c00-4c86-11e8-b3d7-01146121b73d', }, { - name: 'panel_4', + id: '334084f0-52fd-11e8-a160-89cc2ad9e8e2', + name: '23:panel_23', type: 'visualization', - id: 'f8290060-4c88-11e8-b3d7-01146121b73d', }, { - name: 'panel_5', + id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', + name: '31:panel_31', type: 'visualization', - id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', }, { - name: 'panel_6', - type: 'visualization', - id: '9886b410-4c8b-11e8-b3d7-01146121b73d', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + 'aa810aa2-29c9-4a75-b39e-f4f267de1732:control_aa810aa2-29c9-4a75-b39e-f4f267de1732_0_index_pattern', + type: 'index-pattern', }, { - name: 'panel_7', - type: 'visualization', - id: '76e3c090-4c8c-11e8-b3d7-01146121b73d', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + 'aa810aa2-29c9-4a75-b39e-f4f267de1732:control_aa810aa2-29c9-4a75-b39e-f4f267de1732_1_index_pattern', + type: 'index-pattern', }, { - name: 'panel_8', - type: 'visualization', - id: '707665a0-4c8c-11e8-b3d7-01146121b73d', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + 'aa810aa2-29c9-4a75-b39e-f4f267de1732:control_aa810aa2-29c9-4a75-b39e-f4f267de1732_2_index_pattern', + type: 'index-pattern', }, { - name: 'panel_10', - type: 'visualization', - id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', }, { - name: 'panel_11', - type: 'visualization', - id: '129be430-4c93-11e8-b3d7-01146121b73d', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65:indexpattern-datasource-layer-03c34665-471c-49c7-acf1-5a11f517421c', + type: 'index-pattern', }, { - name: 'panel_12', - type: 'visualization', - id: '334084f0-52fd-11e8-a160-89cc2ad9e8e2', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '9271deff-5a61-4665-83fc-f9fdc6bf0c0b:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', }, { - name: 'panel_13', - type: 'visualization', - id: 'f8283bf0-52fd-11e8-a160-89cc2ad9e8e2', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + '9271deff-5a61-4665-83fc-f9fdc6bf0c0b:indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317', + type: 'index-pattern', }, { - name: 'panel_14', - type: 'visualization', - id: '08884800-52fe-11e8-a160-89cc2ad9e8e2', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'aa591c29-1a31-4ee1-a71d-b829c06fd162:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', }, { - name: 'panel_15', - type: 'visualization', - id: 'e6944e50-52fe-11e8-a160-89cc2ad9e8e2', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + 'aa591c29-1a31-4ee1-a71d-b829c06fd162:indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317', + type: 'index-pattern', }, { - name: 'panel_16', - type: 'visualization', - id: '01c413e0-5395-11e8-99bf-1ba7b1bdaa61', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'aa591c29-1a31-4ee1-a71d-b829c06fd162:filter-index-pattern-0', + type: 'index-pattern', }, { - name: 'panel_17', - type: 'visualization', - id: '2edf78b0-5395-11e8-99bf-1ba7b1bdaa61', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'b766e3b8-4544-46ed-99e6-9ecc4847e2a2:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', }, { - name: 'panel_18', - type: 'visualization', - id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + 'b766e3b8-4544-46ed-99e6-9ecc4847e2a2:indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '2e33ade5-96e5-40b4-b460-493e5d4fa834:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + '2e33ade5-96e5-40b4-b460-493e5d4fa834:indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '2e33ade5-96e5-40b4-b460-493e5d4fa834:filter-index-pattern-0', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + 'fb86b32f-fb7a-45cf-9511-f366fef51bbd:indexpattern-datasource-layer-f26e8f7a-4118-4227-bea0-5c02d8b270f7', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + '5d53db36-2d5a-4adc-af7b-cec4c1a294e0:indexpattern-datasource-layer-0c8e136b-a822-4fb3-836d-e06cbea4eea4', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0:filter-index-pattern-0', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '0cc42484-16f7-42ec-b38c-9bf8be69cde7:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + '0cc42484-16f7-42ec-b38c-9bf8be69cde7:indexpattern-datasource-layer-e80cc05e-c52a-4e5f-ac71-4b37274867f5', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: '392b4936-f753-47bc-a98d-a4e41a0a4cd4:indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + name: + '392b4936-f753-47bc-a98d-a4e41a0a4cd4:indexpattern-datasource-layer-8fa993db-c147-4954-adf7-4ff264d42576', + type: 'index-pattern', }, ], migrationVersion: { - dashboard: '7.0.0', + dashboard: '7.14.0', }, attributes: { title: i18n.translate('home.sampleData.flightsSpec.globalFlightDashboardTitle', { defaultMessage: '[Flights] Global Flight Dashboard', }), hits: 0, - description: i18n.translate( - 'home.sampleData.flightsSpec.globalFlightDashboardDescription', - { - defaultMessage: - 'Analyze mock flight data for ES-Air, Logstash Airways, Kibana Airlines and JetBeats', - } - ), + description: i18n.translate('home.sampleData.flightsSpec.globalFlightDashboardDescription', { + defaultMessage: + 'Analyze mock flight data for ES-Air, Logstash Airways, Kibana Airlines and JetBeats', + }), panelsJSON: - '[{"panelIndex":"1","gridData":{"x":0,"y":0,"w":32,"h":7,"i":"1"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_0"},{"panelIndex":"3","gridData":{"x":17,"y":7,"w":23,"h":12,"i":"3"},"embeddableConfig":{"vis":{"colors":{"Average Ticket Price":"#0A50A1","Flight Count":"#82B5D8"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_1"},{"panelIndex":"4","gridData":{"x":0,"y":85,"w":48,"h":15,"i":"4"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_2"},{"panelIndex":"5","gridData":{"x":0,"y":7,"w":17,"h":12,"i":"5"},"embeddableConfig":{"vis":{"colors":{"ES-Air":"#447EBC","JetBeats":"#65C5DB","Kibana Airlines":"#BA43A9","Logstash Airways":"#E5AC0E"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_3"},{"panelIndex":"6","gridData":{"x":24,"y":33,"w":24,"h":14,"i":"6"},"embeddableConfig":{"vis":{"colors":{"Carrier Delay":"#5195CE","Late Aircraft Delay":"#1F78C1","NAS Delay":"#70DBED","No Delay":"#BADFF4","Security Delay":"#052B51","Weather Delay":"#6ED0E0"}}},"version":"6.3.0","panelRefName":"panel_4"},{"panelIndex":"7","gridData":{"x":24,"y":19,"w":24,"h":14,"i":"7"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_5"},{"panelIndex":"10","gridData":{"x":0,"y":35,"w":24,"h":12,"i":"10"},"embeddableConfig":{"vis":{"colors":{"Count":"#1F78C1"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_6"},{"panelIndex":"13","gridData":{"x":10,"y":19,"w":14,"h":8,"i":"13"},"embeddableConfig":{"vis":{"colors":{"Count":"#1F78C1"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_7"},{"panelIndex":"14","gridData":{"x":10,"y":27,"w":14,"h":8,"i":"14"},"embeddableConfig":{"vis":{"colors":{"Count":"#1F78C1"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_8"},{"panelIndex":"21","gridData":{"x":0,"y":62,"w":48,"h":8,"i":"21"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_10"},{"panelIndex":"22","gridData":{"x":32,"y":0,"w":16,"h":7,"i":"22"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_11"},{"panelIndex":"23","gridData":{"x":0,"y":70,"w":48,"h":15,"i":"23"},"embeddableConfig":{"mapCenter":[42.19556096274418,9.536742995308601e-7],"mapZoom":1},"version":"6.3.0","panelRefName":"panel_12"},{"panelIndex":"25","gridData":{"x":0,"y":19,"w":10,"h":8,"i":"25"},"embeddableConfig":{"vis":{"defaultColors":{"0 - 50":"rgb(247,251,255)","100 - 150":"rgb(107,174,214)","150 - 200":"rgb(33,113,181)","200 - 250":"rgb(8,48,107)","50 - 100":"rgb(198,219,239)"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_13"},{"panelIndex":"27","gridData":{"x":0,"y":27,"w":10,"h":8,"i":"27"},"embeddableConfig":{"vis":{"defaultColors":{"0 - 50":"rgb(247,251,255)","100 - 150":"rgb(107,174,214)","150 - 200":"rgb(33,113,181)","200 - 250":"rgb(8,48,107)","50 - 100":"rgb(198,219,239)"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_14"},{"panelIndex":"28","gridData":{"x":0,"y":47,"w":24,"h":15,"i":"28"},"embeddableConfig":{"vis":{"defaultColors":{"0 - 11":"rgb(247,251,255)","11 - 22":"rgb(208,225,242)","22 - 33":"rgb(148,196,223)","33 - 44":"rgb(74,152,201)","44 - 55":"rgb(23,100,171)"},"legendOpen":false}},"version":"6.3.0","panelRefName":"panel_15"},{"panelIndex":"29","gridData":{"x":40,"y":7,"w":8,"h":6,"i":"29"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_16"},{"panelIndex":"30","gridData":{"x":40,"y":13,"w":8,"h":6,"i":"30"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_17"},{"panelIndex":"31","gridData":{"x":24,"y":47,"w":24,"h":15,"i":"31"},"embeddableConfig":{},"version":"6.3.0","panelRefName":"panel_18"}]', + '[{"version":"7.14.0","type":"search","gridData":{"x":0,"y":68,"w":48,"h":15,"i":"4"},"panelIndex":"4","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_4"},{"version":"7.14.0","type":"visualization","gridData":{"x":0,"y":15,"w":24,"h":9,"i":"7"},"panelIndex":"7","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_7"},{"version":"7.14.0","type":"visualization","gridData":{"x":0,"y":57,"w":24,"h":11,"i":"10"},"panelIndex":"10","embeddableConfig":{"vis":{"colors":{"Count":"#1F78C1"},"legendOpen":false},"enhancements":{}},"panelRefName":"panel_10"},{"version":"7.14.0","type":"visualization","gridData":{"x":36,"y":57,"w":12,"h":11,"i":"21"},"panelIndex":"21","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_21"},{"version":"7.14.0","type":"map","gridData":{"x":0,"y":35,"w":24,"h":22,"i":"23"},"panelIndex":"23","embeddableConfig":{"isLayerTOCOpen":true,"enhancements":{},"mapCenter":{"lat":34.65823,"lon":-112.44472,"zoom":4.28},"mapBuffer":{"minLon":-135,"minLat":21.94305,"maxLon":-90,"maxLat":48.9225},"openTOCDetails":[],"hiddenLayers":[]},"panelRefName":"panel_23"},{"version":"7.14.0","type":"visualization","gridData":{"x":24,"y":35,"w":24,"h":22,"i":"31"},"panelIndex":"31","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_31"},{"version":"7.14.0","type":"visualization","gridData":{"x":0,"y":0,"w":32,"h":7,"i":"aa810aa2-29c9-4a75-b39e-f4f267de1732"},"panelIndex":"aa810aa2-29c9-4a75-b39e-f4f267de1732","embeddableConfig":{"savedVis":{"title":"[Flights] Controls","description":"","type":"input_control_vis","params":{"controls":[{"id":"1525098134264","fieldName":"OriginCityName","parent":"","label":"Origin City","type":"list","options":{"type":"terms","multiselect":true,"size":100,"order":"desc"},"indexPatternRefName":"control_aa810aa2-29c9-4a75-b39e-f4f267de1732_0_index_pattern"},{"id":"1525099277699","fieldName":"DestCityName","parent":"1525098134264","label":"Destination City","type":"list","options":{"type":"terms","multiselect":true,"size":100,"order":"desc"},"indexPatternRefName":"control_aa810aa2-29c9-4a75-b39e-f4f267de1732_1_index_pattern"},{"id":"1525099307278","fieldName":"AvgTicketPrice","parent":"","label":"Average Ticket Price","type":"range","options":{"decimalPlaces":0,"step":10},"indexPatternRefName":"control_aa810aa2-29c9-4a75-b39e-f4f267de1732_2_index_pattern"}],"updateFiltersOnChange":false,"useTimeFilter":true,"pinFilters":false},"uiState":{},"data":{"aggs":[],"searchSource":{}}},"hidePanelTitles":true,"enhancements":{}}},{"version":"7.14.0","type":"visualization","gridData":{"x":32,"y":0,"w":16,"h":7,"i":"6afc61f7-e2d5-45a3-9e7a-281160ad3eb9"},"panelIndex":"6afc61f7-e2d5-45a3-9e7a-281160ad3eb9","embeddableConfig":{"savedVis":{"title":"[Flights] Markdown Instructions","description":"","type":"markdown","params":{"fontSize":10,"openLinksInNewTab":true,"markdown":"### Sample Flight data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"uiState":{},"data":{"aggs":[],"searchSource":{}}},"hidePanelTitles":true,"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":7,"w":24,"h":8,"i":"086ac2e9-dd16-4b45-92b8-1e43ff7e3f65"},"panelIndex":"086ac2e9-dd16-4b45-92b8-1e43ff7e3f65","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"03c34665-471c-49c7-acf1-5a11f517421c":{"columns":{"a5b94e30-4e77-4b0a-9187-1d8b13de1456":{"label":"timestamp","dataType":"date","operationType":"date_histogram","sourceField":"timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"3e267327-7317-4310-aee3-320e0f7c1e70":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records"}},"columnOrder":["a5b94e30-4e77-4b0a-9187-1d8b13de1456","3e267327-7317-4310-aee3-320e0f7c1e70"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"hide","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"custom","lowerBound":0,"upperBound":1},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_stacked","layers":[{"layerId":"03c34665-471c-49c7-acf1-5a11f517421c","accessors":["3e267327-7317-4310-aee3-320e0f7c1e70"],"position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"a5b94e30-4e77-4b0a-9187-1d8b13de1456"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-03c34665-471c-49c7-acf1-5a11f517421c"}]},"hidePanelTitles":false,"enhancements":{}},"title":"[Flights] Flight count"},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":7,"w":8,"h":8,"i":"392b4936-f753-47bc-a98d-a4e41a0a4cd4"},"panelIndex":"392b4936-f753-47bc-a98d-a4e41a0a4cd4","embeddableConfig":{"enhancements":{},"attributes":{"title":"[Flights] Total Flights","description":"","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"8fa993db-c147-4954-adf7-4ff264d42576":{"columns":{"81124c45-6ab6-42f4-8859-495d55eb8065":{"label":"Total flights","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true}},"columnOrder":["81124c45-6ab6-42f4-8859-495d55eb8065"],"incompleteColumns":{}}}}},"visualization":{"layerId":"8fa993db-c147-4954-adf7-4ff264d42576","accessor":"81124c45-6ab6-42f4-8859-495d55eb8065"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-8fa993db-c147-4954-adf7-4ff264d42576"}]},"hidePanelTitles":true}},{"version":"7.14.0","type":"lens","gridData":{"x":32,"y":7,"w":8,"h":4,"i":"9271deff-5a61-4665-83fc-f9fdc6bf0c0b"},"panelIndex":"9271deff-5a61-4665-83fc-f9fdc6bf0c0b","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"b4712d43-1e84-4f5b-878d-8e38ba748317":{"columns":{"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0":{"label":"Part of count(kql=\'FlightDelay : true\') / count()","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","filter":{"query":"FlightDelay : true","language":"kuery"},"customLabel":true},"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1":{"label":"Part of count(kql=\'FlightDelay : true\') / count()","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2":{"label":"Part of count(kql=\'FlightDelay : true\') / count()","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"divide","args":["7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1"],"location":{"min":0,"max":41},"text":"count(kql=\'FlightDelay : true\') / count()"}},"references":["7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1"],"customLabel":true},"7e8fe9b1-f45c-4f3d-9561-30febcd357ec":{"label":"Delayed","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"count(kql=\'FlightDelay : true\') / count()","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":1}}},"references":["7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2"],"customLabel":true}},"columnOrder":["7e8fe9b1-f45c-4f3d-9561-30febcd357ec","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2"],"incompleteColumns":{}}}}},"visualization":{"layerId":"b4712d43-1e84-4f5b-878d-8e38ba748317","accessor":"7e8fe9b1-f45c-4f3d-9561-30febcd357ec"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":40,"y":7,"w":8,"h":4,"i":"aa591c29-1a31-4ee1-a71d-b829c06fd162"},"panelIndex":"aa591c29-1a31-4ee1-a71d-b829c06fd162","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"b4712d43-1e84-4f5b-878d-8e38ba748317":{"columns":{"c7851241-5526-499a-960b-357af8c2ce5bX0":{"label":"Part of Delayed","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"c7851241-5526-499a-960b-357af8c2ce5bX1":{"label":"Part of Delayed","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","timeShift":"1w","customLabel":true},"c7851241-5526-499a-960b-357af8c2ce5bX2":{"label":"Part of Delayed","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"subtract","args":[{"type":"function","name":"divide","args":["c7851241-5526-499a-960b-357af8c2ce5bX0","c7851241-5526-499a-960b-357af8c2ce5bX1"],"location":{"min":0,"max":28},"text":"count() / count(shift=\'1w\') "},1],"location":{"min":0,"max":31},"text":"count() / count(shift=\'1w\') - 1"}},"references":["c7851241-5526-499a-960b-357af8c2ce5bX0","c7851241-5526-499a-960b-357af8c2ce5bX1"],"customLabel":true},"c7851241-5526-499a-960b-357af8c2ce5b":{"label":"Delayed vs 1 week earlier","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"count() / count(shift=\'1w\') - 1","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":1}}},"references":["c7851241-5526-499a-960b-357af8c2ce5bX2"],"customLabel":true}},"columnOrder":["c7851241-5526-499a-960b-357af8c2ce5b","c7851241-5526-499a-960b-357af8c2ce5bX2","c7851241-5526-499a-960b-357af8c2ce5bX0","c7851241-5526-499a-960b-357af8c2ce5bX1"],"incompleteColumns":{}}}}},"visualization":{"layerId":"b4712d43-1e84-4f5b-878d-8e38ba748317","accessor":"c7851241-5526-499a-960b-357af8c2ce5b"},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"negate":false,"disabled":false,"type":"phrase","key":"FlightDelay","params":{"query":true},"indexRefName":"filter-index-pattern-0"},"query":{"match_phrase":{"FlightDelay":true}},"$state":{"store":"appState"}}]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317"},{"name":"filter-index-pattern-0","type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":32,"y":11,"w":8,"h":4,"i":"b766e3b8-4544-46ed-99e6-9ecc4847e2a2"},"panelIndex":"b766e3b8-4544-46ed-99e6-9ecc4847e2a2","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"b4712d43-1e84-4f5b-878d-8e38ba748317":{"columns":{"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0":{"label":"Part of Cancelled","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","filter":{"query":"Cancelled : true","language":"kuery"},"customLabel":true},"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1":{"label":"Part of Cancelled","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2":{"label":"Part of Cancelled","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"divide","args":["7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1"],"location":{"min":0,"max":39},"text":"count(kql=\'Cancelled : true\') / count()"}},"references":["7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1"],"customLabel":true},"7e8fe9b1-f45c-4f3d-9561-30febcd357ec":{"label":"Cancelled","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"count(kql=\'Cancelled : true\') / count()","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":1}}},"references":["7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2"],"customLabel":true}},"columnOrder":["7e8fe9b1-f45c-4f3d-9561-30febcd357ec","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1","7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2"],"incompleteColumns":{}}}}},"visualization":{"layerId":"b4712d43-1e84-4f5b-878d-8e38ba748317","accessor":"7e8fe9b1-f45c-4f3d-9561-30febcd357ec"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":40,"y":11,"w":8,"h":4,"i":"2e33ade5-96e5-40b4-b460-493e5d4fa834"},"panelIndex":"2e33ade5-96e5-40b4-b460-493e5d4fa834","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"b4712d43-1e84-4f5b-878d-8e38ba748317":{"columns":{"c7851241-5526-499a-960b-357af8c2ce5bX0":{"label":"Part of Delayed vs 1 week earlier","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"c7851241-5526-499a-960b-357af8c2ce5bX1":{"label":"Part of Delayed vs 1 week earlier","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","timeShift":"1w","customLabel":true},"c7851241-5526-499a-960b-357af8c2ce5bX2":{"label":"Part of Delayed vs 1 week earlier","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"subtract","args":[{"type":"function","name":"divide","args":["c7851241-5526-499a-960b-357af8c2ce5bX0","c7851241-5526-499a-960b-357af8c2ce5bX1"],"location":{"min":0,"max":28},"text":"count() / count(shift=\'1w\') "},1],"location":{"min":0,"max":31},"text":"count() / count(shift=\'1w\') - 1"}},"references":["c7851241-5526-499a-960b-357af8c2ce5bX0","c7851241-5526-499a-960b-357af8c2ce5bX1"],"customLabel":true},"c7851241-5526-499a-960b-357af8c2ce5b":{"label":"Cancelled vs 1 week earlier","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"count() / count(shift=\'1w\') - 1","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":1}}},"references":["c7851241-5526-499a-960b-357af8c2ce5bX2"],"customLabel":true}},"columnOrder":["c7851241-5526-499a-960b-357af8c2ce5b","c7851241-5526-499a-960b-357af8c2ce5bX2","c7851241-5526-499a-960b-357af8c2ce5bX0","c7851241-5526-499a-960b-357af8c2ce5bX1"],"incompleteColumns":{}}}}},"visualization":{"layerId":"b4712d43-1e84-4f5b-878d-8e38ba748317","accessor":"c7851241-5526-499a-960b-357af8c2ce5b"},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"alias":null,"negate":false,"disabled":false,"type":"phrase","key":"Cancelled","params":{"query":true},"indexRefName":"filter-index-pattern-0"},"query":{"match_phrase":{"Cancelled":true}},"$state":{"store":"appState"}}]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317"},{"name":"filter-index-pattern-0","type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":15,"w":24,"h":20,"i":"fb86b32f-fb7a-45cf-9511-f366fef51bbd"},"panelIndex":"fb86b32f-fb7a-45cf-9511-f366fef51bbd","embeddableConfig":{"attributes":{"title":"Cities by delay, cancellation","type":"lens","visualizationType":"lnsDatatable","state":{"datasourceStates":{"indexpattern":{"layers":{"f26e8f7a-4118-4227-bea0-5c02d8b270f7":{"columns":{"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0":{"label":"Top values of OriginCityName","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"OriginCityName","isBucketed":true,"params":{"size":1000,"orderBy":{"type":"alphabetical","fallback":true},"orderDirection":"asc","otherBucket":true,"missingBucket":false}},"52f6f2e9-6242-4c44-be63-b799150e7e60X0":{"label":"Part of Delay %","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","filter":{"query":"FlightDelay : true ","language":"kuery"},"customLabel":true},"52f6f2e9-6242-4c44-be63-b799150e7e60X1":{"label":"Part of Delay %","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"52f6f2e9-6242-4c44-be63-b799150e7e60X2":{"label":"Part of Delay %","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"divide","args":["52f6f2e9-6242-4c44-be63-b799150e7e60X0","52f6f2e9-6242-4c44-be63-b799150e7e60X1"],"location":{"min":0,"max":42},"text":"count(kql=\'FlightDelay : true \') / count()"}},"references":["52f6f2e9-6242-4c44-be63-b799150e7e60X0","52f6f2e9-6242-4c44-be63-b799150e7e60X1"],"customLabel":true},"52f6f2e9-6242-4c44-be63-b799150e7e60":{"label":"Delay %","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"count(kql=\'FlightDelay : true \') / count()","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":0}}},"references":["52f6f2e9-6242-4c44-be63-b799150e7e60X2"],"customLabel":true},"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0":{"label":"Part of Cancel %","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","filter":{"query":"Cancelled: true","language":"kuery"},"customLabel":true},"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1":{"label":"Part of Cancel %","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2":{"label":"Part of Cancel %","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"divide","args":["7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0","7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1"],"location":{"min":0,"max":38},"text":"count(kql=\'Cancelled: true\') / count()"}},"references":["7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0","7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1"],"customLabel":true},"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6":{"label":"Cancel %","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"count(kql=\'Cancelled: true\') / count()","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":0}}},"references":["7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2"],"customLabel":true}},"columnOrder":["3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0","52f6f2e9-6242-4c44-be63-b799150e7e60","52f6f2e9-6242-4c44-be63-b799150e7e60X0","52f6f2e9-6242-4c44-be63-b799150e7e60X1","52f6f2e9-6242-4c44-be63-b799150e7e60X2","7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0","7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1","7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2","7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6"],"incompleteColumns":{}}}}},"visualization":{"columns":[{"isTransposed":false,"columnId":"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0","width":262.75},{"columnId":"52f6f2e9-6242-4c44-be63-b799150e7e60","isTransposed":false,"width":302.5,"colorMode":"cell","palette":{"name":"custom","type":"palette","params":{"steps":5,"stops":[{"color":"#f7e0b8","stop":0.6},{"color":"#e7664c","stop":1}],"name":"custom","colorStops":[{"color":"#f7e0b8","stop":0.2},{"color":"#e7664c","stop":0.6}],"rangeType":"number","rangeMin":0.2,"rangeMax":0.6}},"alignment":"center"},{"columnId":"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6","isTransposed":false,"alignment":"center","colorMode":"cell","palette":{"name":"custom","type":"palette","params":{"steps":5,"stops":[{"color":"#f7e0b8","stop":0.6},{"color":"#e7664c","stop":0.6666666666666666}],"rangeType":"number","name":"custom","colorStops":[{"color":"#f7e0b8","stop":0.2},{"color":"#e7664c","stop":0.6}],"rangeMin":0.2,"rangeMax":0.6}}}],"layerId":"f26e8f7a-4118-4227-bea0-5c02d8b270f7","sorting":{"columnId":"52f6f2e9-6242-4c44-be63-b799150e7e60","direction":"desc"}},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-f26e8f7a-4118-4227-bea0-5c02d8b270f7"}]},"enhancements":{},"hidePanelTitles":false},"title":"[Flights] Most delayed cities"},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":24,"w":24,"h":11,"i":"0cc42484-16f7-42ec-b38c-9bf8be69cde7"},"panelIndex":"0cc42484-16f7-42ec-b38c-9bf8be69cde7","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"e80cc05e-c52a-4e5f-ac71-4b37274867f5":{"columns":{"caf7421e-93a3-439e-ab0a-fbdead93c21c":{"label":"Top values of FlightDelayType","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"FlightDelayType","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"0233d302-ec81-4fbe-96cb-7fac84cf035c"},"orderDirection":"desc","otherBucket":true,"missingBucket":false}},"13ec79e3-9d73-4536-9056-3d92802bb30a":{"label":"timestamp","dataType":"date","operationType":"date_histogram","sourceField":"timestamp","isBucketed":true,"scale":"interval","params":{"interval":"auto"}},"0233d302-ec81-4fbe-96cb-7fac84cf035c":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records"}},"columnOrder":["caf7421e-93a3-439e-ab0a-fbdead93c21c","13ec79e3-9d73-4536-9056-3d92802bb30a","0233d302-ec81-4fbe-96cb-7fac84cf035c"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"bottom"},"valueLabels":"hide","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":true,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_percentage_stacked","layers":[{"layerId":"e80cc05e-c52a-4e5f-ac71-4b37274867f5","accessors":["0233d302-ec81-4fbe-96cb-7fac84cf035c"],"position":"top","seriesType":"bar_percentage_stacked","showGridlines":false,"palette":{"type":"palette","name":"cool"},"xAccessor":"13ec79e3-9d73-4536-9056-3d92802bb30a","splitAccessor":"caf7421e-93a3-439e-ab0a-fbdead93c21c"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-e80cc05e-c52a-4e5f-ac71-4b37274867f5"}]},"hidePanelTitles":false,"enhancements":{}},"title":"[Flights] Delay Type"},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":57,"w":12,"h":11,"i":"5d53db36-2d5a-4adc-af7b-cec4c1a294e0"},"panelIndex":"5d53db36-2d5a-4adc-af7b-cec4c1a294e0","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsPie","state":{"datasourceStates":{"indexpattern":{"layers":{"0c8e136b-a822-4fb3-836d-e06cbea4eea4":{"columns":{"d1cee8bf-34cf-4141-99d7-ff043ee77b56":{"label":"Top values of FlightDelayType","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"FlightDelayType","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"aa152ace-ee2d-447b-b86d-459bef4d7880"},"orderDirection":"desc","otherBucket":true,"missingBucket":false}},"aa152ace-ee2d-447b-b86d-459bef4d7880":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records"}},"columnOrder":["d1cee8bf-34cf-4141-99d7-ff043ee77b56","aa152ace-ee2d-447b-b86d-459bef4d7880"],"incompleteColumns":{}}}}},"visualization":{"shape":"pie","palette":{"type":"palette","name":"cool"},"layers":[{"layerId":"0c8e136b-a822-4fb3-836d-e06cbea4eea4","groups":["d1cee8bf-34cf-4141-99d7-ff043ee77b56"],"metric":"aa152ace-ee2d-447b-b86d-459bef4d7880","numberDisplay":"percent","categoryDisplay":"default","legendDisplay":"default","nestedLegend":false}]},"query":{"query":"","language":"kuery"},"filters":[{"meta":{"type":"phrase","key":"FlightDelayType","params":{"query":"No Delay"},"disabled":false,"negate":true,"alias":null,"indexRefName":"filter-index-pattern-0"},"query":{"match_phrase":{"FlightDelayType":"No Delay"}},"$state":{"store":"appState"}}]},"references":[{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d","name":"indexpattern-datasource-layer-0c8e136b-a822-4fb3-836d-e06cbea4eea4"},{"name":"filter-index-pattern-0","type":"index-pattern","id":"d3d7af60-4c81-11e8-b3d7-01146121b73d"}]},"enhancements":{},"hidePanelTitles":false},"title":"[Flights] Delay Type"}]', optionsJSON: '{"hidePanelTitles":false,"useMargins":true}', version: 1, timeRestore: true, timeTo: 'now', - timeFrom: 'now-24h', + timeFrom: 'now-7d', refreshInterval: { - display: '15 minutes', - pause: false, - section: 2, - value: 900000, + pause: true, + value: 0, }, kibanaSavedObjectMeta: { searchSourceJSON: diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.ts index dff0d8640997..ad7e1d45e290 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.ts @@ -143,12 +143,16 @@ export class SampleDataRegistry { reference.type = embeddableType; reference.id = embeddableId; + const referenceName = reference.name.includes(':') + ? reference.name.split(':')[1] + : reference.name; + const panels = JSON.parse(dashboard.attributes.panelsJSON); const panel = panels.find((panelItem: any) => { - return panelItem.panelRefName === reference.name; + return panelItem.panelRefName === referenceName; }); if (!panel) { - throw new Error(`Unable to find panel for reference: ${reference.name}`); + throw new Error(`Unable to find panel for reference: ${referenceName}`); } panel.embeddableConfig = embeddableConfig; dashboard.attributes.panelsJSON = JSON.stringify(panels); diff --git a/src/plugins/presentation_util/common/lib/test_helpers/function_wrapper.ts b/src/plugins/presentation_util/common/lib/test_helpers/function_wrapper.ts index 4ec02fd622cf..51e82d785e86 100644 --- a/src/plugins/presentation_util/common/lib/test_helpers/function_wrapper.ts +++ b/src/plugins/presentation_util/common/lib/test_helpers/function_wrapper.ts @@ -13,8 +13,15 @@ import { ExpressionFunctionDefinition, } from '../../../../expressions/common'; -type FnType = () => typeof typeSpecs[number] & - ExpressionFunctionDefinition, ExpressionValueBoxed>; +type DefaultFnResultType = typeof typeSpecs[number] & + ExpressionFunctionDefinition< + string, + any, + Record, + ExpressionValueBoxed | Promise> + >; + +type FnType = () => DefaultFnResultType; // It takes a function spec and passes in default args into the spec fn export const functionWrapper = (fnSpec: FnType): ReturnType['fn'] => { diff --git a/src/plugins/presentation_util/common/lib/utils/index.ts b/src/plugins/presentation_util/common/lib/utils/index.ts index eed4acf78b2b..232ec09cf8b0 100644 --- a/src/plugins/presentation_util/common/lib/utils/index.ts +++ b/src/plugins/presentation_util/common/lib/utils/index.ts @@ -7,9 +7,14 @@ */ export * from './dataurl'; -export * from './elastic_logo'; -export * from './elastic_outline'; export * from './httpurl'; -export * from './missing_asset'; export * from './resolve_dataurl'; export * from './url'; + +export async function getElasticLogo() { + return await import('./elastic_logo'); +} + +export async function getElasticOutline() { + return await import('./elastic_outline'); +} diff --git a/src/plugins/presentation_util/public/__stories__/index.tsx b/src/plugins/presentation_util/public/__stories__/index.tsx index 078a16cb8cab..a5633c4a2dd1 100644 --- a/src/plugins/presentation_util/public/__stories__/index.tsx +++ b/src/plugins/presentation_util/public/__stories__/index.tsx @@ -7,3 +7,4 @@ */ export * from './render'; +export * from './wait_for'; diff --git a/src/plugins/presentation_util/public/__stories__/render.tsx b/src/plugins/presentation_util/public/__stories__/render.tsx index 29d95e6bf281..2588d2e3294a 100644 --- a/src/plugins/presentation_util/public/__stories__/render.tsx +++ b/src/plugins/presentation_util/public/__stories__/render.tsx @@ -31,13 +31,11 @@ interface RenderAdditionalProps { handlers?: IInterpreterRenderHandlers; } -export const Render = ({ - renderer, - config, - ...rest -}: Renderer extends () => ExpressionRenderDefinition - ? { renderer: Renderer; config: Config } & RenderAdditionalProps - : { renderer: undefined; config: undefined } & RenderAdditionalProps) => { +export type RenderProps = T extends () => ExpressionRenderDefinition + ? { renderer: T; config: Config } & RenderAdditionalProps + : { renderer: undefined; config: undefined } & RenderAdditionalProps; + +export const Render = ({ renderer, config, ...rest }: RenderProps) => { const { height, width, handlers } = { height: '200px', width: '200px', diff --git a/src/plugins/presentation_util/public/__stories__/wait_for.tsx b/src/plugins/presentation_util/public/__stories__/wait_for.tsx new file mode 100644 index 000000000000..b6421ec7adf5 --- /dev/null +++ b/src/plugins/presentation_util/public/__stories__/wait_for.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 React, { useState, useEffect, useRef, ReactElement } from 'react'; +import { act } from 'react-test-renderer'; +import { Story } from '@storybook/react'; +import { StoryFnReactReturnType } from '@storybook/react/dist/client/preview/types'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +export const waitFor = ( + waitTarget: Promise, + spinner: ReactElement | null = +) => (CurrentStory: Story) => { + const [storyComponent, setStory] = useState(); + const componentIsMounted = useRef(false); + + useEffect(() => { + componentIsMounted.current = true; + return () => { + componentIsMounted.current = false; + }; + }, []); + + useEffect(() => { + if (!storyComponent) { + waitTarget.then((waitedTarget: any) => { + if (!componentIsMounted.current) return; + act(() => { + setStory(); + }); + }); + } + }, [CurrentStory, storyComponent]); + + return storyComponent ?? spinner; +}; diff --git a/src/plugins/telemetry_management_section/README.md b/src/plugins/telemetry_management_section/README.md index 0f795786720c..c23a8591f679 100644 --- a/src/plugins/telemetry_management_section/README.md +++ b/src/plugins/telemetry_management_section/README.md @@ -1,5 +1,5 @@ # Telemetry Management Section -This plugin adds the Advanced Settings section for the Usage Data collection (aka Telemetry). +This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry). The reason for having it separated from the `telemetry` plugin is to avoid circular dependencies. The plugin `advancedSettings` depends on the `home` app that depends on the `telemetry` plugin because of the telemetry banner in the welcome screen. diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/example_security_payload.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/example_security_payload.test.tsx.snap new file mode 100644 index 000000000000..a63044ffc898 --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/example_security_payload.test.tsx.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`example security payload renders as expected 1`] = ` + + { + "@timestamp": "2020-09-22T14:34:56.82202300Z", + "agent": { + "build": { + "original": "version: 7.9.1, compiled: Thu Aug 27 14:50:21 2020, branch: 7.9, commit: b594beb958817dee9b9d908191ed766d483df3ea" + }, + "id": "22dd8544-bcac-46cb-b970-5e681bb99e0b", + "type": "endpoint", + "version": "7.9.1" + }, + "Endpoint": { + "policy": { + "applied": { + "artifacts": { + "global": { + "identifiers": [ + { + "sha256": "6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd", + "name": "endpointpe-v4-blocklist" + }, + { + "sha256": "04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385", + "name": "endpointpe-v4-exceptionlist" + }, + { + "sha256": "1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8", + "name": "endpointpe-v4-model" + }, + { + "sha256": "824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8", + "name": "global-exceptionlist-windows" + } + ], + "version": "1.0.0" + }, + "user": { + "identifiers": [ + { + "sha256": "d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658", + "name": "endpoint-exceptionlist-windows-v1" + } + ], + "version": "1.0.0" + } + } + } + } + }, + "ecs": { + "version": "1.5.0" + }, + "elastic": { + "agent": { + "id": "b2e88aea-2671-402a-828a-957526bac315" + } + }, + "file": { + "path": "C:\\\\Windows\\\\Temp\\\\mimikatz.exe", + "size": 1263880, + "created": "2020-05-19T07:50:06.0Z", + "accessed": "2020-09-22T14:29:19.93531400Z", + "mtime": "2020-09-22T14:29:03.6040000Z", + "directory": "C:\\\\Windows\\\\Temp", + "hash": { + "sha1": "c9fb7f8a4c6b7b12b493a99a8dc6901d17867388", + "sha256": "cb1553a3c88817e4cc774a5a93f9158f6785bd3815447d04b6c3f4c2c4b21ed7", + "md5": "465d5d850f54d9cde767bda90743df30" + }, + "Ext": { + "code_signature": { + "trusted": true, + "subject_name": "Open Source Developer, Benjamin Delpy", + "exists": true, + "status": "trusted" + }, + "malware_classification": { + "identifier": "endpointpe-v4-model", + "score": 0.99956864118576, + "threshold": 0.71, + "version": "0.0.0" + } + } + }, + "host": { + "os": { + "Ext": { + "variant": "Windows 10 Enterprise Evaluation" + }, + "kernel": "2004 (10.0.19041.388)", + "name": "Windows", + "family": "windows", + "version": "2004 (10.0.19041.388)", + "platform": "windows", + "full": "Windows 10 Enterprise Evaluation 2004 (10.0.19041.388)" + } + }, + "event": { + "kind": "alert" + }, + "cluster_uuid": "kLbKvSMcRiiFAR0t8LebDA", + "cluster_name": "elasticsearch" +} + +`; diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/opt_in_security_example_flyout.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/opt_in_security_example_flyout.test.tsx.snap new file mode 100644 index 000000000000..9110926e3963 --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/opt_in_security_example_flyout.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`security flyout renders as expected renders as expected 1`] = ` + + + + +

+ Endpoint security data +

+
+ + + This is a representative sample of the endpoint security alert event that we collect. Endpoint security data is collected only when the Elastic Endpoint is enabled. It includes information about the endpoint configuration and detection events. + + +
+ + + + + + + } + > + + + +
+
+`; diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index de2ac41062d1..014142a2a3d0 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -1,5 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`TelemetryManagementSectionComponent does not show the endpoint link when isSecurityExampleEnabled returns false 1`] = ` + +

+ + + , + } + } + /> +

+

+ + + , + } + } + /> +

+
+`; + exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` <_EuiSplitPanelOuter @@ -79,8 +124,8 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `

, + "endpointSecurityData": + + , } } /> @@ -232,6 +287,19 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "timeZone": null, } } + isSecurityExampleEnabled={ + [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": true, + }, + ], + } + } onQueryMatchChange={[MockFunction]} showAppliesSettingMessage={true} telemetryService={ diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts b/src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx similarity index 54% rename from src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts rename to src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx index 09cd26c9e620..0b22ad5b9c20 100644 --- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts +++ b/src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx @@ -6,8 +6,12 @@ * Side Public License, v 1. */ -import { errors as revealImage } from './dict/reveal_image'; +import React from 'react'; +import { shallowWithIntl } from '@kbn/test/jest'; +import ExampleSecurityPayload from './example_security_payload'; -export const getFunctionErrors = () => ({ - revealImage, +describe('example security payload', () => { + it('renders as expected', () => { + expect(shallowWithIntl()).toMatchSnapshot(); + }); }); diff --git a/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx b/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx new file mode 100644 index 000000000000..6a18ccac59ee --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx @@ -0,0 +1,124 @@ +/* + * 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 { EuiCodeBlock } from '@elastic/eui'; +import * as React from 'react'; + +const exampleSecurityPayload = { + '@timestamp': '2020-09-22T14:34:56.82202300Z', + agent: { + build: { + original: + 'version: 7.9.1, compiled: Thu Aug 27 14:50:21 2020, branch: 7.9, commit: b594beb958817dee9b9d908191ed766d483df3ea', + }, + id: '22dd8544-bcac-46cb-b970-5e681bb99e0b', + type: 'endpoint', + version: '7.9.1', + }, + Endpoint: { + policy: { + applied: { + artifacts: { + global: { + identifiers: [ + { + sha256: '6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd', + name: 'endpointpe-v4-blocklist', + }, + { + sha256: '04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385', + name: 'endpointpe-v4-exceptionlist', + }, + { + sha256: '1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8', + name: 'endpointpe-v4-model', + }, + { + sha256: '824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8', + name: 'global-exceptionlist-windows', + }, + ], + version: '1.0.0', + }, + user: { + identifiers: [ + { + sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + name: 'endpoint-exceptionlist-windows-v1', + }, + ], + version: '1.0.0', + }, + }, + }, + }, + }, + ecs: { + version: '1.5.0', + }, + elastic: { + agent: { + id: 'b2e88aea-2671-402a-828a-957526bac315', + }, + }, + file: { + path: 'C:\\Windows\\Temp\\mimikatz.exe', + size: 1263880, + created: '2020-05-19T07:50:06.0Z', + accessed: '2020-09-22T14:29:19.93531400Z', + mtime: '2020-09-22T14:29:03.6040000Z', + directory: 'C:\\Windows\\Temp', + hash: { + sha1: 'c9fb7f8a4c6b7b12b493a99a8dc6901d17867388', + sha256: 'cb1553a3c88817e4cc774a5a93f9158f6785bd3815447d04b6c3f4c2c4b21ed7', + md5: '465d5d850f54d9cde767bda90743df30', + }, + Ext: { + code_signature: { + trusted: true, + subject_name: 'Open Source Developer, Benjamin Delpy', + exists: true, + status: 'trusted', + }, + malware_classification: { + identifier: 'endpointpe-v4-model', + score: 0.99956864118576, + threshold: 0.71, + version: '0.0.0', + }, + }, + }, + host: { + os: { + Ext: { + variant: 'Windows 10 Enterprise Evaluation', + }, + kernel: '2004 (10.0.19041.388)', + name: 'Windows', + family: 'windows', + version: '2004 (10.0.19041.388)', + platform: 'windows', + full: 'Windows 10 Enterprise Evaluation 2004 (10.0.19041.388)', + }, + }, + event: { + kind: 'alert', + }, + cluster_uuid: 'kLbKvSMcRiiFAR0t8LebDA', + cluster_name: 'elasticsearch', +}; + +const ExampleSecurityPayload: React.FC = () => { + return ( + {JSON.stringify(exampleSecurityPayload, null, 2)} + ); +}; + +// Used for lazy import +// eslint-disable-next-line import/no-default-export +export default ExampleSecurityPayload; diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx new file mode 100644 index 000000000000..74fd7ddd56cb --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 React from 'react'; +import { shallowWithIntl } from '@kbn/test/jest'; +import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout'; + +describe('security flyout renders as expected', () => { + it('renders as expected', () => { + expect(shallowWithIntl()).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx new file mode 100644 index 000000000000..58a82487c25d --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 * as React from 'react'; + +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiPortal, // EuiPortal is a temporary requirement to use EuiFlyout with "ownFocus" + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import { loadingSpinner } from './loading_spinner'; + +interface Props { + onClose: () => void; +} + +const LazyExampleSecurityPayload = React.lazy(() => import('./example_security_payload')); + +/** + * React component for displaying the example data associated with the Telemetry opt-in banner. + */ +export class OptInSecurityExampleFlyout extends React.PureComponent { + render() { + return ( + + + + +

Endpoint security data

+ + + + This is a representative sample of the endpoint security alert event that we + collect. Endpoint security data is collected only when the Elastic Endpoint is + enabled. It includes information about the endpoint configuration and detection + events. + + + + + + + + + + + ); + } +} diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx index a6ad9d4c3dc0..fe6f8e254142 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -21,6 +21,7 @@ describe('TelemetryManagementSectionComponent', () => { it('renders as expected', () => { const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -45,6 +46,7 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={true} enableSaving={true} + isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -54,6 +56,7 @@ describe('TelemetryManagementSectionComponent', () => { it('renders null because query does not match the SEARCH_TERMS', () => { const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -78,6 +81,7 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} enableSaving={true} + isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -94,6 +98,7 @@ describe('TelemetryManagementSectionComponent', () => { showAppliesSettingMessage={false} enableSaving={true} toasts={coreStart.notifications.toasts} + isSecurityExampleEnabled={isSecurityExampleEnabled} docLinks={docLinks} /> @@ -107,6 +112,7 @@ describe('TelemetryManagementSectionComponent', () => { it('renders because query matches the SEARCH_TERMS', () => { const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -129,6 +135,7 @@ describe('TelemetryManagementSectionComponent', () => { telemetryService={telemetryService} onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} + isSecurityExampleEnabled={isSecurityExampleEnabled} enableSaving={true} toasts={coreStart.notifications.toasts} docLinks={docLinks} @@ -154,6 +161,7 @@ describe('TelemetryManagementSectionComponent', () => { it('renders null because allowChangingOptInStatus is false', () => { const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -177,6 +185,7 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={true} enableSaving={true} + isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -192,6 +201,7 @@ describe('TelemetryManagementSectionComponent', () => { it('shows the OptInExampleFlyout', () => { const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -215,6 +225,7 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} enableSaving={true} + isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -229,8 +240,91 @@ describe('TelemetryManagementSectionComponent', () => { } }); + it('shows the OptInSecurityExampleFlyout', () => { + const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + isScreenshotMode: false, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + currentKibanaVersion: 'mock_kibana_version', + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + const toggleExampleComponent = component.find('FormattedMessage > EuiLink[onClick]').at(1); + const updatedView = toggleExampleComponent.simulate('click'); + updatedView.find('OptInSecurityExampleFlyout'); + updatedView.simulate('close'); + } finally { + component.unmount(); + } + }); + + it('does not show the endpoint link when isSecurityExampleEnabled returns false', () => { + const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(false); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + isScreenshotMode: false, + reportOptInStatusChange: false, + currentKibanaVersion: 'mock_kibana_version', + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + + try { + const description = (component.instance() as TelemetryManagementSection).renderDescription(); + expect(isSecurityExampleEnabled).toBeCalled(); + expect(description).toMatchSnapshot(); + } finally { + component.unmount(); + } + }); + it('toggles the OptIn button', async () => { const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -254,6 +348,7 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} enableSaving={true} + isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -280,6 +375,7 @@ describe('TelemetryManagementSectionComponent', () => { it('test the wrapper (for coverage purposes)', () => { const onQueryMatchChange = jest.fn(); + const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -305,6 +401,7 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} enableSaving={true} toasts={coreStart.notifications.toasts} + isSecurityExampleEnabled={isSecurityExampleEnabled} docLinks={docLinks} /> ).html() diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index f46632cb35c7..b0d1b42a9b89 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -15,6 +15,7 @@ import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import type { DocLinksStart, ToastsStart } from 'src/core/public'; import { PRIVACY_STATEMENT_URL } from '../../../telemetry/common/constants'; import { OptInExampleFlyout } from './opt_in_example_flyout'; +import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout'; import { LazyField } from '../../../advanced_settings/public'; import { TrackApplicationView } from '../../../usage_collection/public'; @@ -25,6 +26,7 @@ const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data']; interface Props { telemetryService: TelemetryService; onQueryMatchChange: (searchTermMatches: boolean) => void; + isSecurityExampleEnabled: () => boolean; showAppliesSettingMessage: boolean; enableSaving: boolean; query?: { text: string }; @@ -35,6 +37,7 @@ interface Props { interface State { processing: boolean; showExample: boolean; + showSecurityExample: boolean; queryMatches: boolean | null; enabled: boolean; } @@ -46,6 +49,7 @@ export class TelemetryManagementSection extends Component { this.state = { processing: false, showExample: false, + showSecurityExample: false, queryMatches: props.query ? this.checkQueryMatch(props.query) : null, enabled: this.props.telemetryService.getIsOptedIn() || false, }; @@ -76,8 +80,9 @@ export class TelemetryManagementSection extends Component { } render() { - const { telemetryService } = this.props; - const { showExample, queryMatches, enabled, processing } = this.state; + const { telemetryService, isSecurityExampleEnabled } = this.props; + const { showExample, showSecurityExample, queryMatches, enabled, processing } = this.state; + const securityExampleEnabled = isSecurityExampleEnabled(); if (!telemetryService.getCanChangeOptInStatus()) { return null; @@ -97,6 +102,11 @@ export class TelemetryManagementSection extends Component { /> )} + {showSecurityExample && securityExampleEnabled && ( + + + + )} @@ -172,12 +182,20 @@ export class TelemetryManagementSection extends Component { }; renderDescription = () => { + const { isSecurityExampleEnabled } = this.props; + const securityExampleEnabled = isSecurityExampleEnabled(); const clusterDataLink = ( ); + const endpointSecurityDataLink = ( + + + + ); + return (

@@ -198,13 +216,24 @@ export class TelemetryManagementSection extends Component { />

- + {securityExampleEnabled ? ( + + ) : ( + + )}

); @@ -248,6 +277,15 @@ export class TelemetryManagementSection extends Component { showExample: !this.state.showExample, }); }; + + toggleSecurityExample = () => { + const { isSecurityExampleEnabled } = this.props; + const securityExampleEnabled = isSecurityExampleEnabled(); + if (!securityExampleEnabled) return; + this.setState({ + showSecurityExample: !this.state.showSecurityExample, + }); + }; } // required for lazy loading diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx index 30769683803f..91881dffa52d 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx @@ -12,19 +12,21 @@ import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import type TelemetryManagementSection from './telemetry_management_section'; export type TelemetryManagementSectionWrapperProps = Omit< TelemetryManagementSection['props'], - 'telemetryService' | 'showAppliesSettingMessage' + 'telemetryService' | 'showAppliesSettingMessage' | 'isSecurityExampleEnabled' >; const TelemetryManagementSectionComponent = lazy(() => import('./telemetry_management_section')); export function telemetryManagementSectionWrapper( - telemetryService: TelemetryPluginSetup['telemetryService'] + telemetryService: TelemetryPluginSetup['telemetryService'], + shouldShowSecuritySolutionUsageExample: () => boolean ) { const TelemetryManagementSectionWrapper = (props: TelemetryManagementSectionWrapperProps) => ( }> diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index f39d94954019..db6ea17556ed 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,6 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; +export type { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/public/plugin.tsx b/src/plugins/telemetry_management_section/public/plugin.tsx index 6db05dfe812b..24583260329a 100644 --- a/src/plugins/telemetry_management_section/public/plugin.tsx +++ b/src/plugins/telemetry_management_section/public/plugin.tsx @@ -10,7 +10,7 @@ import React from 'react'; import type { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public'; import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import type { CoreStart, CoreSetup } from 'src/core/public'; +import type { Plugin, CoreStart, CoreSetup } from 'src/core/public'; import { telemetryManagementSectionWrapper, @@ -34,7 +34,17 @@ export interface TelemetryManagementSectionPluginDepsSetup { usageCollection?: UsageCollectionSetup; } -export class TelemetryManagementSectionPlugin { +export interface TelemetryManagementSectionPluginSetup { + toggleSecuritySolutionExample: (enabled: boolean) => void; +} + +export class TelemetryManagementSectionPlugin + implements Plugin { + private showSecuritySolutionExample = false; + private shouldShowSecuritySolutionExample = () => { + return this.showSecuritySolutionExample; + }; + public setup( core: CoreSetup, { @@ -50,16 +60,21 @@ export class TelemetryManagementSectionPlugin { (props) => { return ( - {telemetryManagementSectionWrapper(telemetryService)( - props as TelemetryManagementSectionWrapperProps - )} + {telemetryManagementSectionWrapper( + telemetryService, + this.shouldShowSecuritySolutionExample + )(props as TelemetryManagementSectionWrapperProps)} ); }, true ); - return {}; + return { + toggleSecuritySolutionExample: (enabled: boolean) => { + this.showSecuritySolutionExample = enabled; + }, + }; } public start(core: CoreStart) {} diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index fa0e0bd5f48f..c32d15c336cf 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -33,14 +33,27 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { const [valueInput, setValueInput] = useState(); useEffect(() => { - const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = - services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; + const { stateTransferService, history, data } = services; + const { + originatingApp: value, + embeddableId: embeddableIdValue, + valueInput: valueInputValue, + searchSessionId, + } = stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; + setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); + if (!valueInputValue) { // if there is no value input to load, redirect to the visualize listing page. - services.history.replace(VisualizeConstants.LANDING_PAGE_PATH); + history.replace(VisualizeConstants.LANDING_PAGE_PATH); + } + + if (searchSessionId) { + data.search.session.continue(searchSessionId); + } else { + data.search.session.start(); } }, [services]); diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index c6333e978183..546738bf36c3 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -27,6 +27,7 @@ import { VisualizeConstants } from '../..'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); const [originatingApp, setOriginatingApp] = useState(); + const [embeddableIdValue, setEmbeddableId] = useState(); const { services } = useKibana(); const [eventEmitter] = useState(new EventEmitter()); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(!visualizationIdFromUrl); @@ -55,8 +56,17 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useEffect(() => { - const { originatingApp: value } = - services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; + const { stateTransferService, data } = services; + const { originatingApp: value, searchSessionId, embeddableId } = + stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; + + if (searchSessionId) { + data.search.session.continue(searchSessionId); + } else { + data.search.session.start(); + } + + setEmbeddableId(embeddableId); setOriginatingApp(value); }, [services]); @@ -65,7 +75,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { return () => { eventEmitter.removeAllListeners(); }; - }, [eventEmitter]); + }, [eventEmitter, services]); return ( { setHasUnsavedChanges={setHasUnsavedChanges} visEditorRef={visEditorRef} onAppLeave={onAppLeave} + embeddableId={embeddableIdValue} /> ); }; diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index f6ef1caf9c9e..ad933e597f0a 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -61,13 +61,21 @@ const TopNav = ({ const session = embeddableHandler.openInspector(); setInspectorSession(session); }, [embeddableHandler]); + + const doReload = useCallback(async () => { + // start a new session to make sure all data is up to date + services.data.search.session.start(); + + await visInstance.embeddableHandler.reload(); + }, [visInstance.embeddableHandler, services.data.search.session]); + const handleRefresh = useCallback( (_payload: any, isUpdate?: boolean) => { if (isUpdate === false) { - visInstance.embeddableHandler.reload(); + doReload(); } }, - [visInstance.embeddableHandler] + [doReload] ); const config = useMemo(() => { @@ -185,7 +193,7 @@ const TopNav = ({ .getAutoRefreshFetch$() .subscribe(async (done) => { try { - await visInstance.embeddableHandler.reload(); + await doReload(); } finally { done(); } @@ -193,7 +201,7 @@ const TopNav = ({ return () => { autoRefreshFetchSub.unsubscribe(); }; - }, [services.data.query.timefilter.timefilter, visInstance.embeddableHandler]); + }, [services.data.query.timefilter.timefilter, doReload]); return isChromeVisible ? ( /** diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 82757e9a8e35..ed361bbdb104 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -81,6 +81,7 @@ export const getTopNavConfig = ( embeddableId, }: TopNavConfigParams, { + data, application, chrome, history, @@ -154,17 +155,17 @@ export const getTopNavConfig = ( saveOptions.dashboardId === 'new' ? '#/create' : `#/view/${saveOptions.dashboardId}`; } - if (newlyCreated && stateTransfer) { + if (stateTransfer) { stateTransfer.navigateToWithEmbeddablePackage(app, { state: { type: VISUALIZE_EMBEDDABLE_TYPE, input: { savedObjectId: id }, - embeddableId, + embeddableId: savedVis.copyOnSave ? undefined : embeddableId, + searchSessionId: data.search.session.getSessionId(), }, path, }); } else { - // TODO: need the same thing here? application.navigateToApp(app, { path }); } } else { @@ -214,6 +215,7 @@ export const getTopNavConfig = ( } as VisualizeInput, embeddableId, type: VISUALIZE_EMBEDDABLE_TYPE, + searchSessionId: data.search.session.getSessionId(), }; stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { state }); }; @@ -394,6 +396,7 @@ export const getTopNavConfig = ( } as VisualizeInput, embeddableId, type: VISUALIZE_EMBEDDABLE_TYPE, + searchSessionId: data.search.session.getSessionId(), }; const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 9eda709e58c3..8898076d7ddb 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -40,9 +40,10 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( savedObjectsPublic, } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { + id: '', timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), - id: '', + searchSessionId: data.search.session.getSessionId(), })) as VisualizeEmbeddableContract; embeddableHandler.getOutput$().subscribe((output) => { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index b5ddbdf6d10a..00c3545034b3 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -215,6 +215,7 @@ export class VisualizePlugin const { renderApp } = await import('./application'); const unmount = renderApp(params, services); return () => { + data.search.session.clear(); params.element.classList.remove('visAppWrapper'); unlistenParentHistory(); unmount(); diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index 20b18583d0d7..5a3ec9d8fc86 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Add one more saved object to cancel it', async () => { - await testSubjects.click('savedObjectTitle[Flights]-Average-Ticket-Price'); + await testSubjects.click('savedObjectTitle[Flights]-Destination-Weather'); await a11y.testAppSnapshot(); }); diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts index 2a6c290172a9..c1c8ff402a32 100644 --- a/test/accessibility/apps/dashboard_panel.ts +++ b/test/accessibility/apps/dashboard_panel.ts @@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('dashboard panel open ', async () => { - const header = await dashboardPanelActions.getPanelHeading('[Flights] Airline Carrier'); + const header = await dashboardPanelActions.getPanelHeading('[Flights] Flight count'); await dashboardPanelActions.toggleContextMenu(header); await a11y.testAppSnapshot(); // doing this again will close the Context Menu, so that next snapshot can start clean. @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('dashboard panel inspect', async () => { - await dashboardPanelActions.openInspectorByTitle('[Flights] Airline Carrier'); + await dashboardPanelActions.openInspectorByTitle('[Flights] Flight count'); await a11y.testAppSnapshot(); }); @@ -61,9 +61,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('dashboard panel full screen', async () => { - const header = await dashboardPanelActions.getPanelHeading('[Flights] Airline Carrier'); + const header = await dashboardPanelActions.getPanelHeading('[Flights] Flight count'); await dashboardPanelActions.toggleContextMenu(header); - await dashboardPanelActions.clickContextMenuMoreItem(); await testSubjects.click('embeddablePanelAction-togglePanel'); await a11y.testAppSnapshot(); diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index c681ad325e56..1e029bc1e04d 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ elasticsearchIndicesCreated: { kibana_sample_data_flights: 13059 }, - kibanaSavedObjectsLoaded: 23, + kibanaSavedObjectsLoaded: 11, }); }); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index a35fda2f53ed..adb99d0d42d0 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const log = getService('log'); const security = getService('security'); - const pieChart = getService('pieChart'); + const elasticChart = getService('elasticChart'); const renderable = getService('renderable'); const dashboardExpect = getService('dashboardExpect'); const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); @@ -89,17 +89,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const toTime = `${todayYearMonthDay} @ 23:59:59.999`; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); const panelCount = await PageObjects.dashboard.getPanelCount(); - expect(panelCount).to.be(18); + expect(panelCount).to.be(17); }); it('should render visualizations', async () => { await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - log.debug('Checking pie charts rendered'); - await pieChart.expectPieSliceCount(4); - log.debug('Checking area, bar and heatmap charts rendered'); - await dashboardExpect.seriesElementCount(15); + log.debug('Checking charts rendered'); + await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); log.debug('Checking saved searches rendered'); await dashboardExpect.savedSearchRowCount(10); log.debug('Checking input controls rendered'); @@ -107,8 +105,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Checking tag cloud rendered'); await dashboardExpect.tagCloudWithValuesFound(['Sunny', 'Rain', 'Clear', 'Cloudy', 'Hail']); log.debug('Checking vega chart rendered'); - const tsvb = await find.existsByCssSelector('.vgaVis__view'); - expect(tsvb).to.be(true); + expect(await find.existsByCssSelector('.vgaVis__view')).to.be(true); }); it('should launch sample logs data set dashboard', async () => { diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 49b2ad8f9646..91aec66966df 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); - const esArchiver = getService('esArchiver'); const log = getService('log'); const inspector = getService('inspector'); const retry = getService('retry'); @@ -198,14 +197,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/103252 - describe.skip('switch index patterns', () => { - before(async () => { - await esArchiver.loadIfNeeded( - 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' - ); - }); - + describe('switch index pattern mode', () => { beforeEach(async () => { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); @@ -215,41 +207,32 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.setDropLastBucket(true); await PageObjects.visualBuilder.clickDataTab('metric'); await PageObjects.timePicker.setAbsoluteRange( - 'Sep 22, 2019 @ 00:00:00.000', - 'Sep 23, 2019 @ 00:00:00.000' + 'Sep 19, 2015 @ 06:31:44.000', + 'Sep 22, 2015 @ 18:31:44.000' ); }); - after(async () => { - await security.testUser.restoreDefaults(); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); - await PageObjects.visualize.initTests(); - }); - const switchIndexTest = async (useKibanaIndexes: boolean) => { await PageObjects.visualBuilder.clickPanelOptions('metric'); await PageObjects.visualBuilder.setIndexPatternValue('', false); - const value = await PageObjects.visualBuilder.getMetricValue(); - expect(value).to.eql('0'); - // Sometimes popovers take some time to appear in Firefox (#71979) await retry.tryForTime(20000, async () => { - await PageObjects.visualBuilder.setIndexPatternValue('with-timefield', useKibanaIndexes); + await PageObjects.visualBuilder.setIndexPatternValue('logstash-*', useKibanaIndexes); await PageObjects.visualBuilder.waitForIndexPatternTimeFieldOptionsLoaded(); - await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp'); + await PageObjects.visualBuilder.selectIndexPatternTimeField('@timestamp'); }); const newValue = await PageObjects.visualBuilder.getMetricValue(); - expect(newValue).to.eql('1'); + expect(newValue).to.eql('156'); }; - it('should be able to switch using text mode selection', async () => { - await switchIndexTest(false); - }); - it('should be able to switch combo box mode selection', async () => { await switchIndexTest(true); }); + + it('should be able to switch using text mode selection', async () => { + await switchIndexTest(false); + }); }); describe('browser history changes', () => { diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts index 5e3dd2019d0a..8cd98b17f5d3 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -17,6 +17,7 @@ const createAlertingAuthorizationMock = () => { filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), getAugmentedRuleTypesWithAuthorization: jest.fn(), + getSpaceId: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 50a1b9d84ff6..b3cd47d47dbc 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -81,6 +81,7 @@ export class AlertingAuthorization { private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; private readonly exemptConsumerIds: string[]; + private readonly spaceId: Promise; constructor({ alertTypeRegistry, @@ -101,6 +102,8 @@ export class AlertingAuthorization { // manually authorize each rule type in the management UI. this.exemptConsumerIds = exemptConsumerIds; + this.spaceId = getSpace(request).then((maybeSpace) => maybeSpace?.id); + this.featuresIds = getSpace(request) .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) .then( @@ -138,6 +141,10 @@ export class AlertingAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } + public async getSpaceId(): Promise { + return this.spaceId; + } + /* * This method exposes the private 'augmentRuleTypesWithAuthorization' to be * used by the RAC/Alerts client diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts index 1698708aeb77..6e1fd115aace 100644 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts @@ -19,7 +19,6 @@ export interface ResponseHit { } export interface SearchServiceParams { - index: string; environment?: string; kuery?: string; serviceName?: string; @@ -31,6 +30,10 @@ export interface SearchServiceParams { percentileThresholdValue?: number; } +export interface SearchServiceFetchParams extends SearchServiceParams { + index: string; +} + export interface SearchServiceValue { histogram: HistogramItem[]; value: string; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index 88d1823f05cc..26a44180b837 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -27,7 +27,7 @@ export function RumHome() { const { isSmall, isXXL } = useBreakPoints(); - const envStyle = isSmall ? {} : { maxWidth: 200 }; + const envStyle = isSmall ? {} : { maxWidth: 500 }; return ( @@ -59,7 +59,7 @@ export function RumHome() { function PageHeader() { const { isSmall } = useBreakPoints(); - const envStyle = isSmall ? {} : { maxWidth: 200 }; + const envStyle = isSmall ? {} : { maxWidth: 400 }; return (
diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx index f4e39c37e289..cfc57d3b3e4a 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx @@ -70,6 +70,11 @@ const chartTheme: PartialTheme = { }, }; +// Log based axis cannot start a 0. Use a small positive number instead. +const yAxisDomain = { + min: 0.00001, +}; + interface CorrelationsChartProps { field?: string; value?: string; @@ -140,7 +145,10 @@ export function CorrelationsChart({ const histogram = replaceHistogramDotsWithBars(originalHistogram); return ( -
+
{ setIsFlyoutVisible(true); @@ -147,13 +148,17 @@ export function Correlations() { {isFlyoutVisible && ( setIsFlyoutVisible(false)} > -

+

{CORRELATIONS_TITLE}   (enableInspectEsQueries); + const { + ccsWarning, + log, error, histograms, percentileThresholdValue, @@ -76,7 +87,6 @@ export function MlLatencyCorrelations({ onClose }: Props) { cancelFetch, overallHistogram: originalOverallHistogram, } = useCorrelations({ - index: 'apm-*', ...{ ...{ environment, @@ -286,9 +296,10 @@ export function MlLatencyCorrelations({ onClose }: Props) { - + + {ccsWarning && ( + <> + + +

+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', + { + defaultMessage: + 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', + } + )} +

+
+ + )} {overallHistogram !== undefined ? ( <> -

+

{i18n.translate( 'xpack.apm.correlations.latencyCorrelations.chartTitle', { @@ -341,32 +376,58 @@ export function MlLatencyCorrelations({ onClose }: Props) { ) : null} - {histograms.length > 0 && selectedHistogram !== undefined && ( - +
+ {histograms.length > 0 && selectedHistogram !== undefined && ( + + )} + {histograms.length < 1 && progress > 0.99 ? ( + <> + + + + + + ) : null} +
+ {log.length > 0 && displayLog && ( + + + {log.map((d, i) => { + const splitItem = d.split(': '); + return ( +

+ + {splitItem[0]} {splitItem[1]} + +

+ ); + })} +
+
)} - {histograms.length < 1 && progress > 0.99 ? ( - <> - - - - - - ) : null} ); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts index 2baeb63fa4a2..05cb367a9fde 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts @@ -21,7 +21,6 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { ApmPluginStartDeps } from '../../../plugin'; interface CorrelationsOptions { - index: string; environment?: string; kuery?: string; serviceName?: string; @@ -37,6 +36,7 @@ interface RawResponse { values: SearchServiceValue[]; overallHistogram: HistogramItem[]; log: string[]; + ccsWarning: boolean; } export const useCorrelations = (params: CorrelationsOptions) => { @@ -106,6 +106,8 @@ export const useCorrelations = (params: CorrelationsOptions) => { }; return { + ccsWarning: rawResponse?.ccsWarning ?? false, + log: rawResponse?.log ?? [], error, histograms: rawResponse?.values ?? [], percentileThresholdValue: diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index b4644068fd78..a3b0ec0ac66d 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -117,6 +117,7 @@ export function getServiceColumns({ )} { + const { agentName, canShowDashboard, environment, serviceName } = args; + + const KibanaContext = createKibanaReactContext(({ + application: { + capabilities: { dashboard: { show: canShowDashboard } }, + }, + http: { basePath: { get: () => '' } }, + } as unknown) as Partial); + + return ( + + + + + + + + ); + }, + ], +}; + +export const Example: Story = () => { + return ; +}; +Example.args = { + agentName: 'iOS/swift', + canShowDashboard: true, + environment: 'testEnvironment', + serviceName: 'testServiceName', +}; diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx new file mode 100644 index 000000000000..b8b0cfa3054d --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 { composeStories } from '@storybook/testing-react'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { + ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED, +} from '../../../../../common/environment_filter_values'; +import * as stories from './analyze_data_button.stories'; + +const { Example } = composeStories(stories); + +describe('AnalyzeDataButton', () => { + describe('with a non-RUM and non-mobile agent', () => { + it('renders nothing', () => { + render(); + + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + }); + }); + + describe('with no dashboard show capabilities', () => { + it('renders nothing', () => { + render(); + + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + }); + }); + + describe('with a RUM agent', () => { + it('uses a ux dataType', () => { + render(); + + expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + ); + }); + }); + + describe('with a mobile agent', () => { + it('uses a mobile dataType', () => { + render(); + + expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + ); + }); + }); + + describe('with no environment', () => { + it('does not include the environment', () => { + render(); + + expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + ); + }); + }); + + describe('with environment not defined', () => { + it('does not include the environment', () => { + render(); + + expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + ); + }); + }); + + describe('with environment all', () => { + it('uses ALL_VALUES', () => { + render(); + + expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx new file mode 100644 index 000000000000..d8ff7fdf47c5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + createExploratoryViewUrl, + SeriesUrl, +} from '../../../../../../observability/public'; +import { ALL_VALUES_SELECTED } from '../../../../../../observability/public'; +import { + isIosAgentName, + isRumAgentName, +} from '../../../../../common/agent_name'; +import { + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '../../../../../common/elasticsearch_fieldnames'; +import { + ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED, +} from '../../../../../common/environment_filter_values'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; + +function getEnvironmentDefinition(environment?: string) { + switch (environment) { + case ENVIRONMENT_ALL.value: + return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] }; + case ENVIRONMENT_NOT_DEFINED.value: + case undefined: + return {}; + default: + return { [SERVICE_ENVIRONMENT]: [environment] }; + } +} + +export function AnalyzeDataButton() { + const { agentName, serviceName } = useApmServiceContext(); + const { services } = useKibana(); + const { urlParams } = useUrlParams(); + const { rangeTo, rangeFrom, environment } = urlParams; + const basepath = services.http?.basePath.get(); + const canShowDashboard = services.application?.capabilities.dashboard.show; + + if ( + (isRumAgentName(agentName) || isIosAgentName(agentName)) && + canShowDashboard + ) { + const href = createExploratoryViewUrl( + { + 'apm-series': { + dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', + time: { from: rangeFrom, to: rangeTo }, + reportType: 'kpi-over-time', + reportDefinitions: { + [SERVICE_NAME]: [serviceName], + ...getEnvironmentDefinition(environment), + }, + operationType: 'average', + isNew: true, + } as SeriesUrl, + }, + basepath + ); + + return ( + + + {i18n.translate('xpack.apm.analyzeDataButton.label', { + defaultMessage: 'Analyze data', + })} + + + ); + } + + return null; +} diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx similarity index 66% rename from x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx rename to x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 2e10c853f542..ee5ed91dfb46 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -5,45 +5,33 @@ * 2.0. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { + EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiPageHeaderProps, EuiTitle, - EuiBetaBadge, - EuiToolTip, - EuiButtonEmpty, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; -import { ApmMainTemplate } from './apm_main_template'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context'; -import { enableServiceOverview } from '../../../../common/ui_settings_keys'; +import React from 'react'; import { + isIosAgentName, isJavaAgentName, isRumAgentName, - isIosAgentName, -} from '../../../../common/agent_name'; -import { ServiceIcons } from '../../shared/service_icons'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; -import { - SERVICE_NAME, - SERVICE_ENVIRONMENT, -} from '../../../../common/elasticsearch_fieldnames'; -import { Correlations } from '../../app/correlations'; -import { SearchBar } from '../../shared/search_bar'; -import { - createExploratoryViewUrl, - SeriesUrl, -} from '../../../../../observability/public'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; -import { useApmRouter } from '../../../hooks/use_apm_router'; +} from '../../../../../common/agent_name'; +import { enableServiceOverview } from '../../../../../common/ui_settings_keys'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb'; +import { useApmParams } from '../../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../../hooks/use_apm_router'; +import { Correlations } from '../../../app/correlations'; +import { SearchBar } from '../../../shared/search_bar'; +import { ServiceIcons } from '../../../shared/service_icons'; +import { ApmMainTemplate } from '../apm_main_template'; +import { AnalyzeDataButton } from './analyze_data_button'; type Tab = NonNullable[0] & { key: @@ -105,7 +93,9 @@ function TemplateWithContext({ -

{serviceName}

+

+ {serviceName} +

@@ -115,7 +105,7 @@ function TemplateWithContext({ - + @@ -132,53 +122,6 @@ function TemplateWithContext({ ); } -function AnalyzeDataButton({ serviceName }: { serviceName: string }) { - const { agentName } = useApmServiceContext(); - const { services } = useKibana(); - const { urlParams } = useUrlParams(); - const { rangeTo, rangeFrom, environment } = urlParams; - const basepath = services.http?.basePath.get(); - - if (isRumAgentName(agentName) || isIosAgentName(agentName)) { - const href = createExploratoryViewUrl( - { - 'apm-series': { - dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', - time: { from: rangeFrom, to: rangeTo }, - reportType: 'kpi-over-time', - reportDefinitions: { - [SERVICE_NAME]: [serviceName], - ...(!!environment && ENVIRONMENT_NOT_DEFINED.value !== environment - ? { [SERVICE_ENVIRONMENT]: [environment] } - : {}), - }, - operationType: 'average', - isNew: true, - } as SeriesUrl, - }, - basepath - ); - - return ( - - - {i18n.translate('xpack.apm.analyzeDataButton.label', { - defaultMessage: 'Analyze data', - })} - - - ); - } - - return null; -} - function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { const { agentName } = useApmServiceContext(); const { core, config } = useApmPluginContext(); diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts index 6d608f7751f3..10514ddcbdf6 100644 --- a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as t from 'io-ts'; import { CoreSetup, CoreStart, @@ -14,7 +13,7 @@ import { import { promisify } from 'util'; import { unzip } from 'zlib'; import { Artifact } from '../../../../fleet/server'; -import { sourceMapRt } from '../../routes/source_maps'; +import { SourceMap } from '../../routes/source_maps'; import { APMPluginStartDependencies } from '../../types'; import { getApmPackgePolicies } from './get_apm_package_policies'; import { APM_SERVER, PackagePolicy } from './register_fleet_policy_callbacks'; @@ -23,7 +22,7 @@ export interface ApmArtifactBody { serviceName: string; serviceVersion: string; bundleFilepath: string; - sourceMap: t.TypeOf; + sourceMap: SourceMap; } export type ArtifactSourceMap = Omit & { body: ApmArtifactBody; diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index f939a9c39c63..aab8025a7679 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -64,13 +64,7 @@ Object { }, }, ], - "must_not": Array [ - Object { - "term": Object { - "service.environment": "staging", - }, - }, - ], + "must_not": Array [], }, }, "size": 0, @@ -512,13 +506,7 @@ Object { }, }, ], - "must_not": Array [ - Object { - "term": Object { - "service.environment": "staging", - }, - }, - ], + "must_not": Array [], }, }, "size": 0, @@ -566,13 +554,7 @@ Object { }, }, ], - "must_not": Array [ - Object { - "term": Object { - "service.environment": "staging", - }, - }, - ], + "must_not": Array [], }, }, "size": 0, diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.test.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.test.ts new file mode 100644 index 000000000000..ba5e318a1901 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { getEsFilter } from './get_es_filter'; + +describe('getEfFilters', function () { + it('should return environment in include filters', function () { + const result = getEsFilter({ + browser: ['Chrome'], + environment: 'production', + }); + + expect(result).toEqual([ + { terms: { 'user_agent.name': ['Chrome'] } }, + { term: { 'service.environment': 'production' } }, + ]); + }); + + it('should not return environment in exclude filters', function () { + const result = getEsFilter( + { browserExcluded: ['Chrome'], environment: 'production' }, + true + ); + + expect(result).toEqual([{ terms: { 'user_agent.name': ['Chrome'] } }]); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts index 5f587f82e979..76ef9fb95089 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts @@ -34,5 +34,8 @@ export function getEsFilter(uiFilters: UxUIFilters, exclude?: boolean) { }; }) as ESFilter[]; - return [...mappedFilters, ...environmentQuery(uiFilters.environment)]; + return [ + ...mappedFilters, + ...(exclude ? [] : environmentQuery(uiFilters.environment)), + ]; } diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index 155cb1f4615b..90d24b6587f4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -7,6 +7,7 @@ import { shuffle, range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; import { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; import { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; import { fetchTransactionDurationPercentiles } from './query_percentiles'; @@ -16,6 +17,7 @@ import { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; import type { AsyncSearchProviderProgress, SearchServiceParams, + SearchServiceFetchParams, SearchServiceValue, } from '../../../../common/search_strategies/correlations/types'; import { computeExpectationsAndRanges } from './utils/aggregation_utils'; @@ -28,11 +30,14 @@ const currentTimeAsString = () => new Date().toISOString(); export const asyncSearchServiceProvider = ( esClient: ElasticsearchClient, - params: SearchServiceParams + getApmIndices: () => Promise, + searchServiceParams: SearchServiceParams, + includeFrozen: boolean ) => { let isCancelled = false; let isRunning = true; let error: Error; + let ccsWarning = false; const log: string[] = []; const logMessage = (message: string) => log.push(`${currentTimeAsString()}: ${message}`); @@ -63,7 +68,15 @@ export const asyncSearchServiceProvider = ( }; const fetchCorrelations = async () => { + let params: SearchServiceFetchParams | undefined; + try { + const indices = await getApmIndices(); + params = { + ...searchServiceParams, + index: indices['apm_oss.transactionIndices'], + }; + // 95th percentile to be displayed as a marker in the log log chart const { totalDocs, @@ -172,7 +185,7 @@ export const asyncSearchServiceProvider = ( async function* fetchTransactionDurationHistograms() { for (const item of shuffle(fieldValuePairs)) { - if (item === undefined || isCancelled) { + if (params === undefined || item === undefined || isCancelled) { isRunning = false; return; } @@ -222,10 +235,15 @@ export const asyncSearchServiceProvider = ( yield undefined; } } catch (e) { - // don't fail the whole process for individual correlation queries, just add the error to the internal log. + // don't fail the whole process for individual correlation queries, + // just add the error to the internal log and check if we'd want to set the + // cross-cluster search compatibility warning to true. logMessage( `Failed to fetch correlation/kstest for '${item.field}/${item.value}'` ); + if (params?.index.includes(':')) { + ccsWarning = true; + } yield undefined; } } @@ -247,6 +265,10 @@ export const asyncSearchServiceProvider = ( error = e; } + if (error !== undefined && params?.index.includes(':')) { + ccsWarning = true; + } + isRunning = false; }; @@ -256,6 +278,7 @@ export const asyncSearchServiceProvider = ( const sortedValues = values.sort((a, b) => b.correlation - a.correlation); return { + ccsWarning, error, log, isRunning, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts index 5d4af3e80f8b..aeb76c37e526 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts @@ -11,7 +11,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { rangeRt } from '../../../routes/default_api_types'; import { getCorrelationsFilters } from '../../correlations/get_filters'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -40,7 +40,7 @@ export const getTermsQuery = ( }; interface QueryParams { - params: SearchServiceParams; + params: SearchServiceFetchParams; fieldName?: string; fieldValue?: string; } diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts index f63c36f90d72..94a708f67860 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts @@ -10,7 +10,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; @@ -40,7 +40,7 @@ export interface BucketCorrelation { } export const getTransactionDurationCorrelationRequest = ( - params: SearchServiceParams, + params: SearchServiceFetchParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], @@ -95,7 +95,7 @@ export const getTransactionDurationCorrelationRequest = ( export const fetchTransactionDurationCorrelation = async ( esClient: ElasticsearchClient, - params: SearchServiceParams, + params: SearchServiceFetchParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts index 0fbdfef405e0..8aa54e243eec 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts @@ -9,7 +9,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { Field } from './query_field_value_pairs'; @@ -37,7 +37,7 @@ export const hasPrefixToInclude = (fieldName: string) => { }; export const getRandomDocsRequest = ( - params: SearchServiceParams + params: SearchServiceFetchParams ): estypes.SearchRequest => ({ index: params.index, body: { @@ -56,7 +56,7 @@ export const getRandomDocsRequest = ( export const fetchTransactionDurationFieldCandidates = async ( esClient: ElasticsearchClient, - params: SearchServiceParams + params: SearchServiceFetchParams ): Promise<{ fieldCandidates: Field[] }> => { const { index } = params; // Get all fields with keyword mapping diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts index 8fde9d3ab137..23928565da08 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts @@ -11,7 +11,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { AsyncSearchProviderProgress, - SearchServiceParams, + SearchServiceFetchParams, } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; @@ -26,7 +26,7 @@ type FieldValuePairs = FieldValuePair[]; export type Field = string; export const getTermsAggRequest = ( - params: SearchServiceParams, + params: SearchServiceFetchParams, fieldName: string ): estypes.SearchRequest => ({ index: params.index, @@ -46,7 +46,7 @@ export const getTermsAggRequest = ( export const fetchTransactionDurationFieldValuePairs = async ( esClient: ElasticsearchClient, - params: SearchServiceParams, + params: SearchServiceFetchParams, fieldCandidates: Field[], progress: AsyncSearchProviderProgress ): Promise => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts index 3d623a4df8c3..e9cec25673c6 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts @@ -7,12 +7,12 @@ import { ElasticsearchClient } from 'kibana/server'; import { estypes } from '@elastic/elasticsearch'; -import { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; export const getTransactionDurationRangesRequest = ( - params: SearchServiceParams, + params: SearchServiceFetchParams, ranges: estypes.AggregationsAggregationRange[] ): estypes.SearchRequest => ({ index: params.index, @@ -35,7 +35,7 @@ export const getTransactionDurationRangesRequest = ( */ export const fetchTransactionDurationFractions = async ( esClient: ElasticsearchClient, - params: SearchServiceParams, + params: SearchServiceFetchParams, ranges: estypes.AggregationsAggregationRange[] ): Promise<{ fractions: number[]; totalDocCount: number }> => { const resp = await esClient.search( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts index 6f61ecbfdcf0..045caabeab26 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts @@ -13,13 +13,13 @@ import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldname import type { HistogramItem, ResponseHit, - SearchServiceParams, + SearchServiceFetchParams, } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; export const getTransactionDurationHistogramRequest = ( - params: SearchServiceParams, + params: SearchServiceFetchParams, interval: number, fieldName?: string, fieldValue?: string @@ -42,7 +42,7 @@ export const getTransactionDurationHistogramRequest = ( export const fetchTransactionDurationHistogram = async ( esClient: ElasticsearchClient, - params: SearchServiceParams, + params: SearchServiceFetchParams, interval: number, fieldName?: string, fieldValue?: string diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts index c4d1abf24b4d..0f897f2e9236 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts @@ -10,14 +10,14 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; const HISTOGRAM_INTERVALS = 1000; export const getHistogramIntervalRequest = ( - params: SearchServiceParams + params: SearchServiceFetchParams ): estypes.SearchRequest => ({ index: params.index, body: { @@ -32,7 +32,7 @@ export const getHistogramIntervalRequest = ( export const fetchTransactionDurationHistogramInterval = async ( esClient: ElasticsearchClient, - params: SearchServiceParams + params: SearchServiceFetchParams ): Promise => { const resp = await esClient.search(getHistogramIntervalRequest(params)); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts index 6ee5dd6bcdf8..ba57de2cfde3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts @@ -19,7 +19,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; @@ -32,7 +32,7 @@ const getHistogramRangeSteps = (min: number, max: number, steps: number) => { }; export const getHistogramIntervalRequest = ( - params: SearchServiceParams + params: SearchServiceFetchParams ): estypes.SearchRequest => ({ index: params.index, body: { @@ -47,7 +47,7 @@ export const getHistogramIntervalRequest = ( export const fetchTransactionDurationHistogramRangeSteps = async ( esClient: ElasticsearchClient, - params: SearchServiceParams + params: SearchServiceFetchParams ): Promise => { const steps = 100; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts index c80f5d836c0e..cb302f19a000 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts @@ -10,7 +10,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { SIGNIFICANT_VALUE_DIGITS } from './constants'; @@ -28,7 +28,7 @@ interface ResponseHit { } export const getTransactionDurationPercentilesRequest = ( - params: SearchServiceParams, + params: SearchServiceFetchParams, percents?: number[], fieldName?: string, fieldValue?: string @@ -58,7 +58,7 @@ export const getTransactionDurationPercentilesRequest = ( export const fetchTransactionDurationPercentiles = async ( esClient: ElasticsearchClient, - params: SearchServiceParams, + params: SearchServiceFetchParams, percents?: number[], fieldName?: string, fieldValue?: string diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts index 9074e7e0809b..0e813a18fdf4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts @@ -10,7 +10,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; @@ -27,7 +27,7 @@ interface ResponseHit { } export const getTransactionDurationRangesRequest = ( - params: SearchServiceParams, + params: SearchServiceFetchParams, rangesSteps: number[], fieldName?: string, fieldValue?: string @@ -65,7 +65,7 @@ export const getTransactionDurationRangesRequest = ( export const fetchTransactionDurationRanges = async ( esClient: ElasticsearchClient, - params: SearchServiceParams, + params: SearchServiceFetchParams, rangesSteps: number[], fieldName?: string, fieldValue?: string diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts index 09775cb2eb03..401cda97afef 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts @@ -9,6 +9,8 @@ import type { estypes } from '@elastic/elasticsearch'; import { SearchStrategyDependencies } from 'src/plugins/data/server'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; + import { apmCorrelationsSearchStrategyProvider, PartialSearchRequest, @@ -94,10 +96,19 @@ const clientSearchMock = ( }; }; +const getApmIndicesMock = async () => + ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + 'apm_oss.transactionIndices': 'apm-*', + } as ApmIndicesConfig); + describe('APM Correlations search strategy', () => { describe('strategy interface', () => { it('returns a custom search strategy with a `search` and `cancel` function', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const searchStrategy = await apmCorrelationsSearchStrategyProvider( + getApmIndicesMock, + false + ); expect(typeof searchStrategy.search).toBe('function'); expect(typeof searchStrategy.cancel).toBe('function'); }); @@ -106,12 +117,14 @@ describe('APM Correlations search strategy', () => { describe('search', () => { let mockClientFieldCaps: jest.Mock; let mockClientSearch: jest.Mock; + let mockGetApmIndicesMock: jest.Mock; let mockDeps: SearchStrategyDependencies; let params: Required['params']; beforeEach(() => { mockClientFieldCaps = jest.fn(clientFieldCapsMock); mockClientSearch = jest.fn(clientSearchMock); + mockGetApmIndicesMock = jest.fn(getApmIndicesMock); mockDeps = ({ esClient: { asCurrentUser: { @@ -121,7 +134,6 @@ describe('APM Correlations search strategy', () => { }, } as unknown) as SearchStrategyDependencies; params = { - index: 'apm-*', start: '2020', end: '2021', }; @@ -130,7 +142,13 @@ describe('APM Correlations search strategy', () => { describe('async functionality', () => { describe('when no params are provided', () => { it('throws an error', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const searchStrategy = await apmCorrelationsSearchStrategyProvider( + mockGetApmIndicesMock, + false + ); + + expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(0); + expect(() => searchStrategy.search({}, {}, mockDeps)).toThrow( 'Invalid request parameters.' ); @@ -139,8 +157,14 @@ describe('APM Correlations search strategy', () => { describe('when no ID is provided', () => { it('performs a client search with params', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const searchStrategy = await apmCorrelationsSearchStrategyProvider( + mockGetApmIndicesMock, + false + ); await searchStrategy.search({ params }, {}, mockDeps).toPromise(); + + expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); + const [[request]] = mockClientSearch.mock.calls; expect(request.index).toEqual('apm-*'); @@ -179,7 +203,10 @@ describe('APM Correlations search strategy', () => { describe('when an ID with params is provided', () => { it('retrieves the current request', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const searchStrategy = await apmCorrelationsSearchStrategyProvider( + mockGetApmIndicesMock, + false + ); const response = await searchStrategy .search({ params }, {}, mockDeps) .toPromise(); @@ -190,6 +217,7 @@ describe('APM Correlations search strategy', () => { .search({ id: searchStrategyId, params }, {}, mockDeps) .toPromise(); + expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); expect(response2).toEqual( expect.objectContaining({ id: searchStrategyId }) ); @@ -201,11 +229,16 @@ describe('APM Correlations search strategy', () => { mockClientSearch .mockReset() .mockRejectedValueOnce(new Error('client error')); - const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const searchStrategy = await apmCorrelationsSearchStrategyProvider( + mockGetApmIndicesMock, + false + ); const response = await searchStrategy .search({ params }, {}, mockDeps) .toPromise(); + expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); + expect(response).toEqual( expect.objectContaining({ isRunning: true }) ); @@ -213,11 +246,15 @@ describe('APM Correlations search strategy', () => { }); it('triggers the subscription only once', async () => { - expect.assertions(1); - const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + expect.assertions(2); + const searchStrategy = await apmCorrelationsSearchStrategyProvider( + mockGetApmIndicesMock, + false + ); searchStrategy .search({ params }, {}, mockDeps) .subscribe((response) => { + expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); expect(response).toEqual( expect.objectContaining({ loaded: 0, isRunning: true }) ); @@ -227,12 +264,16 @@ describe('APM Correlations search strategy', () => { describe('response', () => { it('sends an updated response on consecutive search calls', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider(); + const searchStrategy = await apmCorrelationsSearchStrategyProvider( + mockGetApmIndicesMock, + false + ); const response1 = await searchStrategy .search({ params }, {}, mockDeps) .toPromise(); + expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); expect(typeof response1.id).toEqual('string'); expect(response1).toEqual( expect.objectContaining({ loaded: 0, isRunning: true }) @@ -244,6 +285,7 @@ describe('APM Correlations search strategy', () => { .search({ id: response1.id, params }, {}, mockDeps) .toPromise(); + expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); expect(response2.id).toEqual(response1.id); expect(response2).toEqual( expect.objectContaining({ loaded: 100, isRunning: false }) diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts index 8f2e6913c0d0..3601f19ad705 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts @@ -19,6 +19,8 @@ import type { SearchServiceValue, } from '../../../../common/search_strategies/correlations/types'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; + import { asyncSearchServiceProvider } from './async_search_service'; export type PartialSearchRequest = IKibanaSearchRequest; @@ -26,10 +28,10 @@ export type PartialSearchResponse = IKibanaSearchResponse<{ values: SearchServiceValue[]; }>; -export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy< - PartialSearchRequest, - PartialSearchResponse -> => { +export const apmCorrelationsSearchStrategyProvider = ( + getApmIndices: () => Promise, + includeFrozen: boolean +): ISearchStrategy => { const asyncSearchServiceMap = new Map< string, ReturnType @@ -65,7 +67,9 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy< } else { getAsyncSearchServiceState = asyncSearchServiceProvider( deps.esClient.asCurrentUser, - request.params + getApmIndices, + request.params, + includeFrozen ); } @@ -73,6 +77,7 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy< const id = request.id ?? uuid(); const { + ccsWarning, error, log, isRunning, @@ -102,6 +107,7 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy< isRunning, isPartial: isRunning, rawResponse: { + ccsWarning, log, took, values, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 3a7eb738dd3b..d28e43d9cb97 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -16,6 +16,7 @@ import { PluginInitializerContext, } from 'src/core/server'; import { isEmpty, mapValues, once } from 'lodash'; +import { SavedObjectsClient } from '../../../../src/core/server'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; import { APMConfig, APMXPackConfig, APM_SERVER_FEATURE_ID } from '.'; @@ -248,12 +249,24 @@ export class APMPlugin }); // search strategies for async partial search results - if (plugins.data?.search?.registerSearchStrategy !== undefined) { - plugins.data.search.registerSearchStrategy( - 'apmCorrelationsSearchStrategy', - apmCorrelationsSearchStrategyProvider() - ); - } + core.getStartServices().then(([coreStart]) => { + (async () => { + const savedObjectsClient = new SavedObjectsClient( + coreStart.savedObjects.createInternalRepository() + ); + + plugins.data.search.registerSearchStrategy( + 'apmCorrelationsSearchStrategy', + apmCorrelationsSearchStrategyProvider( + boundGetApmIndices, + await coreStart.uiSettings + .asScopedToClient(savedObjectsClient) + .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN) + ) + ); + })(); + }); + return { config$: mergedConfig$, getApmIndices: boundGetApmIndices, diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index a01c9dd1579b..afe18b33c482 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -138,10 +138,12 @@ const getMigrationCheckRoute = createApmServerRoute({ const fleetPluginStart = await plugins.fleet.start(); const securityPluginStart = await plugins.security.start(); const hasRequiredRole = isSuperuser({ securityPluginStart, request }); - const cloudAgentPolicy = await getCloudAgentPolicy({ - savedObjectsClient, - fleetPluginStart, - }); + const cloudAgentPolicy = hasRequiredRole + ? await getCloudAgentPolicy({ + savedObjectsClient, + fleetPluginStart, + }) + : undefined; return { has_cloud_agent_policy: !!cloudAgentPolicy, has_cloud_apm_package_policy: !!getApmPackagePolicy(cloudAgentPolicy), diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts index f6d160e68a76..d92bad31cd8d 100644 --- a/x-pack/plugins/apm/server/routes/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -5,9 +5,9 @@ * 2.0. */ import Boom from '@hapi/boom'; -import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { SavedObjectsClientContract } from 'kibana/server'; +import { jsonRt } from '@kbn/io-ts-utils'; import { createApmArtifact, deleteApmArtifact, @@ -17,6 +17,7 @@ import { import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; import { createApmServerRoute } from './create_apm_server_route'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { stringFromBufferRt } from '../utils/string_from_buffer_rt'; export const sourceMapRt = t.intersection([ t.type({ @@ -32,6 +33,8 @@ export const sourceMapRt = t.intersection([ }), ]); +export type SourceMap = t.TypeOf; + const listSourceMapRoute = createApmServerRoute({ endpoint: 'GET /api/apm/sourcemaps', options: { tags: ['access:apm'] }, @@ -62,7 +65,10 @@ const uploadSourceMapRoute = createApmServerRoute({ service_name: t.string, service_version: t.string, bundle_filepath: t.string, - sourcemap: jsonRt.pipe(sourceMapRt), + sourcemap: t + .union([t.string, stringFromBufferRt]) + .pipe(jsonRt) + .pipe(sourceMapRt), }), }), handler: async ({ params, plugins, core }) => { diff --git a/x-pack/plugins/apm/server/utils/string_from_buffer_rt.test.ts b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.test.ts new file mode 100644 index 000000000000..4e21215cac8b --- /dev/null +++ b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { isRight } from 'fp-ts/lib/Either'; +import { stringFromBufferRt } from './string_from_buffer_rt'; + +const sourceMap = { + version: 3, + file: 'static/js/main.chunk.js', + sources: [ + '/foo/src/index.css', + '/foo/src/App.js', + 'webpack:///./src/index.css?bb0a', + '/foo/src/index.js', + '/foo/src/reportWebVitals.js', + ], + sourcesContent: [ + "// Imports\nimport ___CSS_LOADER_API_IMPORT___ from \"../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(true);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"body {\\n margin: 0;\\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\\n sans-serif;\\n -webkit-font-smoothing: antialiased;\\n -moz-osx-font-smoothing: grayscale;\\n}\\n\\ncode {\\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\\n monospace;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://src/index.css\"],\"names\":[],\"mappings\":\"AAAA;EACE,SAAS;EACT;;cAEY;EACZ,mCAAmC;EACnC,kCAAkC;AACpC;;AAEA;EACE;aACW;AACb\",\"sourcesContent\":[\"body {\\n margin: 0;\\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\\n sans-serif;\\n -webkit-font-smoothing: antialiased;\\n -moz-osx-font-smoothing: grayscale;\\n}\\n\\ncode {\\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\\n monospace;\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n", + 'import React from "react";\nimport {\n BrowserRouter as Router,\n Switch,\n Route,\n Link\n} from "react-router-dom";\n\n// This site has 3 pages, all of which are rendered\n// dynamically in the browser (not server rendered).\n//\n// Although the page does not ever refresh, notice how\n// React Router keeps the URL up to date as you navigate\n// through the site. This preserves the browser history,\n// making sure things like the back button and bookmarks\n// work properly.\n\nexport default function App() {\n return (\n \n
\n
    \n
  • \n Home\n
  • \n
  • \n About\n
  • \n
  • \n Dashboard\n
  • \n
  • \n Error\n
  • \n
\n\n
\n\n {/*\n A looks through all its children \n elements and renders the first one whose path\n matches the current URL. Use a any time\n you have multiple routes, but you want only one\n of them to render at a time\n */}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n
\n );\n}\n\n// You can think of these components as "pages"\n// in your app.\n\nfunction Home() {\n return (\n
\n

HOME

\n
\n );\n}\n\nfunction About() {\n return (\n
\n

about

\n
\n );\n}\n\nfunction Dashboard() {\n return (\n
\n

Dashboard

\n
\n );\n}\n\nfunction ErrorPage() {\n throw new Error(\'Boomm\')\n return (\n
\n

error

\n
\n );\n}\n', + "var api = require(\"!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n var content = require(\"!!../node_modules/css-loader/dist/cjs.js??ref--5-oneOf-4-1!../node_modules/postcss-loader/src/index.js??postcss!./index.css\");\n\n content = content.__esModule ? content.default : content;\n\n if (typeof content === 'string') {\n content = [[module.id, content, '']];\n }\n\nvar options = {};\n\noptions.insert = \"head\";\noptions.singleton = false;\n\nvar update = api(content, options);\n\n\nif (module.hot) {\n if (!content.locals || module.hot.invalidate) {\n var isEqualLocals = function isEqualLocals(a, b, isNamedExport) {\n if (!a && b || a && !b) {\n return false;\n }\n\n var p;\n\n for (p in a) {\n if (isNamedExport && p === 'default') {\n // eslint-disable-next-line no-continue\n continue;\n }\n\n if (a[p] !== b[p]) {\n return false;\n }\n }\n\n for (p in b) {\n if (isNamedExport && p === 'default') {\n // eslint-disable-next-line no-continue\n continue;\n }\n\n if (!a[p]) {\n return false;\n }\n }\n\n return true;\n};\n var oldLocals = content.locals;\n\n module.hot.accept(\n \"!!../node_modules/css-loader/dist/cjs.js??ref--5-oneOf-4-1!../node_modules/postcss-loader/src/index.js??postcss!./index.css\",\n function () {\n content = require(\"!!../node_modules/css-loader/dist/cjs.js??ref--5-oneOf-4-1!../node_modules/postcss-loader/src/index.js??postcss!./index.css\");\n\n content = content.__esModule ? content.default : content;\n\n if (typeof content === 'string') {\n content = [[module.id, content, '']];\n }\n\n if (!isEqualLocals(oldLocals, content.locals)) {\n module.hot.invalidate();\n\n return;\n }\n\n oldLocals = content.locals;\n\n update(content);\n }\n )\n }\n\n module.hot.dispose(function() {\n update();\n });\n}\n\nmodule.exports = content.locals || {};", + "/*eslint-disable import/first */\nimport { init as initApm } from '@elastic/apm-rum'\ninitApm({\n serviceName: 'fleet-source-map-client',\n serverUrl: 'http://localhost:8200',\n // serverUrl: 'https://776d64ec093b47ff86c752f62baa8f51.apm.us-west1.gcp.cloud.es.io:443',\n serviceVersion: '1.0.0',\n environment: 'production'\n})\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport './index.css';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\nReactDOM.render(\n \n \n ,\n document.getElementById('root')\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n", + "const reportWebVitals = onPerfEntry => {\n if (onPerfEntry && onPerfEntry instanceof Function) {\n import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n getCLS(onPerfEntry);\n getFID(onPerfEntry);\n getFCP(onPerfEntry);\n getLCP(onPerfEntry);\n getTTFB(onPerfEntry);\n });\n }\n};\n\nexport default reportWebVitals;\n", + ], + mappings: + ';;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;ACNA;AACA;AAQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAVA;AAAA;AAAA;AAAA;AAAA;AAeA;AAAA;AAAA;AAAA;AASA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAVA;AAAA;AAAA;AAAA;AAAA;AAzBA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AA2CA;AAGA;AACA;AAjDA;AACA;AAiDA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AAPA;AACA;AAOA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AAPA;AACA;AAOA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AAPA;AACA;AAOA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AARA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC5FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AALA;AAOA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAOA;AACA;AACA;AAAA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1BA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;A', + sourceRoot: '', +}; + +describe('stringFromBufferRt', () => { + describe('decode', () => { + it('converts from buffer to string', () => { + const sourceMapBuffer = Buffer.from(JSON.stringify(sourceMap)); + const decoded = stringFromBufferRt.decode(sourceMapBuffer); + if (isRight(decoded)) { + expect(decoded.right).toEqual(JSON.stringify(sourceMap)); + } else { + expect(true).toBeFalsy(); + } + }); + }); + describe('encode', () => { + it('converts from string to buffer', () => { + const encoded = stringFromBufferRt.encode(JSON.stringify(sourceMap)); + expect(encoded).toEqual(Buffer.from(JSON.stringify(sourceMap))); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/utils/string_from_buffer_rt.ts b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.ts new file mode 100644 index 000000000000..3e9304361c86 --- /dev/null +++ b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; + +export const stringFromBufferRt = new t.Type( + 'stringFromBufferRt', + t.string.is, + (input, context) => { + return Buffer.isBuffer(input) + ? t.success(input.toString('utf-8')) + : t.failure(input, context, 'Input is not a Buffer'); + }, + (str) => { + return Buffer.from(str); + } +); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js index fa831cacbcb1..4f621bdd94b3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js @@ -5,7 +5,7 @@ * 2.0. */ -import { elasticLogo } from '../../../../../../../src/plugins/presentation_util/common/lib'; +import { getElasticLogo } from '../../../../../../../src/plugins/presentation_util/common/lib'; export const fontStyle = { type: 'style', @@ -23,16 +23,19 @@ export const fontStyle = { 'font-family:Chalkboard, serif;font-weight:bolder;font-style:normal;text-decoration:underline;color:pink;text-align:center;font-size:14px;line-height:21px', }; -export const containerStyle = { - type: 'containerStyle', - border: '3px dotted blue', - borderRadius: '5px', - padding: '10px', - backgroundColor: 'red', - backgroundImage: `url(${elasticLogo})`, - opacity: 0.5, - backgroundSize: 'contain', - backgroundRepeat: 'no-repeat', +export const getContainerStyle = async () => { + const { elasticLogo } = await getElasticLogo(); + return { + type: 'containerStyle', + border: '3px dotted blue', + borderRadius: '5px', + padding: '10px', + backgroundColor: 'red', + backgroundImage: `url(${elasticLogo})`, + opacity: 0.5, + backgroundSize: 'contain', + backgroundRepeat: 'no-repeat', + }; }; export const defaultStyle = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js index 85e062f454bc..d2a7cd5565ec 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js @@ -14,6 +14,7 @@ const errors = getFunctionErrors().alterColumn; describe('alterColumn', () => { const fn = functionWrapper(alterColumn); + const nameColumnIndex = testTable.columns.findIndex(({ name }) => name === 'name'); const timeColumnIndex = testTable.columns.findIndex(({ name }) => name === 'time'); const priceColumnIndex = testTable.columns.findIndex(({ name }) => name === 'price'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js index d5621943bcca..5bdc013eff59 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js @@ -11,8 +11,8 @@ import { functionWrapper } from '../../../../../../src/plugins/presentation_util import { caseFn } from './case'; describe('case', () => { - const fn = functionWrapper(caseFn); let testScheduler; + const fn = functionWrapper(caseFn); beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js index 7a3599f47ec8..15c7ccdbf5ce 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js @@ -6,8 +6,8 @@ */ import { - elasticLogo, functionWrapper, + getElasticLogo, } from '../../../../../../src/plugins/presentation_util/common/lib'; import { getFunctionErrors } from '../../../i18n'; import { containerStyle } from './containerStyle'; @@ -17,14 +17,21 @@ const errors = getFunctionErrors().containerStyle; describe('containerStyle', () => { const fn = functionWrapper(containerStyle); - describe('default output', () => { - const result = fn(null); + let elasticLogo; + beforeEach(async () => { + elasticLogo = (await getElasticLogo()).elasticLogo; + }); + describe('default output', () => { it('returns a containerStyle', () => { + const result = fn(null); + expect(result).toHaveProperty('type', 'containerStyle'); }); it('all style properties except `overflow` are omitted if args not provided', () => { + const result = fn(null); + expect(Object.keys(result)).toHaveLength(2); expect(result).toHaveProperty('type'); expect(result).toHaveProperty('overflow'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts index cfef618bee39..6feb22b2ef15 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts @@ -15,6 +15,7 @@ const errors = getFunctionErrors().csv; describe('csv', () => { const fn = functionWrapper(csv); + const expected: Datatable = { type: 'datatable', columns: [ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts index 254efd9f5f0d..6f785f1b9d47 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts @@ -95,11 +95,5 @@ describe('dropdownControl', () => { ?.value ).toHaveProperty('column', 'price'); }); - - it('sets column to undefined if no args are provided', () => { - expect( - fn(testTable, {}, {} as ExecutionContext)?.value - ).toHaveProperty('column', undefined); - }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js index edc2c1db18f6..f81e3ae24130 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js @@ -15,8 +15,8 @@ const inStock = (datatable) => of(datatable.rows[0].in_stock); const returnFalse = () => of(false); describe('filterrows', () => { - const fn = functionWrapper(filterrows); let testScheduler; + const fn = functionWrapper(filterrows); beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js index df576a6a2507..fbfcdef07611 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js @@ -12,6 +12,7 @@ import { ifFn } from './if'; describe('if', () => { const fn = functionWrapper(ifFn); + let testScheduler; beforeEach(() => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js index 45b26cd25937..862560e5643d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js @@ -7,61 +7,67 @@ import expect from '@kbn/expect'; import { - elasticLogo, - elasticOutline, + getElasticLogo, + getElasticOutline, + functionWrapper, } from '../../../../../../src/plugins/presentation_util/common/lib'; -// import { image } from './image'; +import { image } from './image'; // TODO: the test was not running and is not up to date -describe.skip('image', () => { - const fn = jest.fn(); +describe('image', () => { + const fn = functionWrapper(image); - it('returns an image object using a dataUrl', () => { - const result = fn(null, { dataurl: elasticOutline, mode: 'cover' }); + let elasticLogo; + let elasticOutline; + beforeEach(async () => { + elasticLogo = (await getElasticLogo()).elasticLogo; + elasticOutline = (await getElasticOutline()).elasticOutline; + }); + + it('returns an image object using a dataUrl', async () => { + const result = await fn(null, { dataurl: elasticOutline, mode: 'cover' }); expect(result).to.have.property('type', 'image'); }); describe('args', () => { describe('dataurl', () => { - it('sets the source of the image using dataurl', () => { - const result = fn(null, { dataurl: elasticOutline }); + it('sets the source of the image using dataurl', async () => { + const result = await fn(null, { dataurl: elasticOutline }); expect(result).to.have.property('dataurl', elasticOutline); }); - it.skip('sets the source of the image using url', () => { + it.skip('sets the source of the image using url', async () => { // This is skipped because functionWrapper doesn't use the actual // interpreter and doesn't resolve aliases - const result = fn(null, { url: elasticOutline }); + const result = await fn(null, { url: elasticOutline }); expect(result).to.have.property('dataurl', elasticOutline); }); - it('defaults to the elasticLogo if not provided', () => { - const result = fn(null); + it('defaults to the elasticLogo if not provided', async () => { + const result = await fn(null); expect(result).to.have.property('dataurl', elasticLogo); }); }); - describe('mode', () => { - it('sets the mode', () => { - it('to contain', () => { - const result = fn(null, { mode: 'contain' }); - expect(result).to.have.property('mode', 'contain'); - }); + describe('sets the mode', () => { + it('to contain', async () => { + const result = await fn(null, { mode: 'contain' }); + expect(result).to.have.property('mode', 'contain'); + }); - it('to cover', () => { - const result = fn(null, { mode: 'cover' }); - expect(result).to.have.property('mode', 'cover'); - }); + it('to cover', async () => { + const result = await fn(null, { mode: 'cover' }); + expect(result).to.have.property('mode', 'cover'); + }); - it('to stretch', () => { - const result = fn(null, { mode: 'stretch' }); - expect(result).to.have.property('mode', 'stretch'); - }); + it('to stretch', async () => { + const result = await fn(null, { mode: 'stretch' }); + expect(result).to.have.property('mode', '100% 100%'); + }); - it("defaults to 'contain' if not provided", () => { - const result = fn(null); - expect(result).to.have.property('mode', 'contain'); - }); + it("defaults to 'contain' if not provided", async () => { + const result = await fn(null); + expect(result).to.have.property('mode', 'contain'); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts index c3e64e48b23f..e661a15cea3a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts @@ -7,8 +7,9 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; + import { - elasticLogo, + getElasticLogo, resolveWithMissingImage, } from '../../../../../../src/plugins/presentation_util/common/lib'; @@ -29,10 +30,9 @@ export interface Return { dataurl: string; } -export function image(): ExpressionFunctionDefinition<'image', null, Arguments, Return> { +export function image(): ExpressionFunctionDefinition<'image', null, Arguments, Promise> { const { help, args: argHelp } = getFunctionHelp().image; const errors = getFunctionErrors().image; - return { name: 'image', aliases: [], @@ -45,7 +45,7 @@ export function image(): ExpressionFunctionDefinition<'image', null, Arguments, types: ['string', 'null'], help: argHelp.dataurl, aliases: ['_', 'url'], - default: elasticLogo, + default: null, }, mode: { types: ['string'], @@ -54,13 +54,17 @@ export function image(): ExpressionFunctionDefinition<'image', null, Arguments, options: Object.values(ImageMode), }, }, - fn: (input, { dataurl, mode }) => { + fn: async (input, { dataurl, mode }) => { if (!mode || !Object.values(ImageMode).includes(mode)) { throw errors.invalidImageMode(); } + const { elasticLogo } = await getElasticLogo(); - const modeStyle = mode === 'stretch' ? '100% 100%' : mode; + if (dataurl === null) { + dataurl = elasticLogo; + } + const modeStyle = mode === 'stretch' ? '100% 100%' : mode; return { type: 'image', mode: modeStyle, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js index 6438e2a4d19c..5f44428abf03 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js @@ -16,6 +16,7 @@ const errors = getFunctionErrors().progress; // TODO: this test was not running and is not up to date describe.skip('progress', () => { const fn = functionWrapper(progress); + const value = 0.33; it('returns a render as progress', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js index 3248af550409..1725af3522ba 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js @@ -8,7 +8,7 @@ import { functionWrapper } from '../../../../../../src/plugins/presentation_util/common/lib'; import { DEFAULT_ELEMENT_CSS } from '../../../common/lib/constants'; import { testTable } from './__fixtures__/test_tables'; -import { fontStyle, containerStyle } from './__fixtures__/test_styles'; +import { fontStyle, getContainerStyle } from './__fixtures__/test_styles'; import { render } from './render'; const renderTable = { @@ -25,7 +25,12 @@ const renderTable = { describe('render', () => { const fn = functionWrapper(render); - it('returns a render', () => { + let containerStyle; + beforeEach(async () => { + containerStyle = await getContainerStyle(); + }); + + it('returns a render', async () => { const result = fn(renderTable, { as: 'debug', css: '".canvasRenderEl { background-color: red; }"', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js index 97f0552721cc..42569e26e426 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js @@ -6,8 +6,8 @@ */ import { - elasticLogo, - elasticOutline, + getElasticLogo, + getElasticOutline, functionWrapper, } from '../../../../../../src/plugins/presentation_util/common/lib'; import { repeatImage } from './repeat_image'; @@ -15,55 +15,62 @@ import { repeatImage } from './repeat_image'; describe('repeatImage', () => { const fn = functionWrapper(repeatImage); - it('returns a render as repeatImage', () => { - const result = fn(10); + let elasticLogo; + let elasticOutline; + beforeEach(async () => { + elasticLogo = await (await getElasticLogo()).elasticLogo; + elasticOutline = await (await getElasticOutline()).elasticOutline; + }); + + it('returns a render as repeatImage', async () => { + const result = await fn(10); expect(result).toHaveProperty('type', 'render'); expect(result).toHaveProperty('as', 'repeatImage'); }); describe('args', () => { describe('image', () => { - it('sets the source of the repeated image', () => { - const result = fn(10, { image: elasticLogo }).value; + it('sets the source of the repeated image', async () => { + const result = (await fn(10, { image: elasticLogo })).value; expect(result).toHaveProperty('image', elasticLogo); }); - it('defaults to the Elastic outline logo', () => { - const result = fn(100000).value; + it('defaults to the Elastic outline logo', async () => { + const result = (await fn(100000)).value; expect(result).toHaveProperty('image', elasticOutline); }); }); describe('size', () => { - it('sets the size of the image', () => { - const result = fn(-5, { size: 200 }).value; + it('sets the size of the image', async () => { + const result = (await fn(-5, { size: 200 })).value; expect(result).toHaveProperty('size', 200); }); - it('defaults to 100', () => { - const result = fn(-5).value; + it('defaults to 100', async () => { + const result = (await fn(-5)).value; expect(result).toHaveProperty('size', 100); }); }); describe('max', () => { - it('sets the maximum number of a times the image is repeated', () => { - const result = fn(100000, { max: 20 }).value; + it('sets the maximum number of a times the image is repeated', async () => { + const result = (await fn(100000, { max: 20 })).value; expect(result).toHaveProperty('max', 20); }); - it('defaults to 1000', () => { - const result = fn(100000).value; + it('defaults to 1000', async () => { + const result = (await fn(100000)).value; expect(result).toHaveProperty('max', 1000); }); }); describe('emptyImage', () => { - it('returns repeatImage object with emptyImage as undefined', () => { - const result = fn(100000, { emptyImage: elasticLogo }).value; + it('returns repeatImage object with emptyImage as undefined', async () => { + const result = (await fn(100000, { emptyImage: elasticLogo })).value; expect(result).toHaveProperty('emptyImage', elasticLogo); }); - it('sets emptyImage to null', () => { - const result = fn(100000).value; + it('sets emptyImage to null', async () => { + const result = (await fn(100000)).value; expect(result).toHaveProperty('emptyImage', null); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts index 904b2478760a..751573e27183 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts @@ -7,7 +7,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { - elasticOutline, + getElasticOutline, resolveWithMissingImage, } from '../../../../../../src/plugins/presentation_util/common/lib'; import { Render } from '../../../types'; @@ -32,10 +32,9 @@ export function repeatImage(): ExpressionFunctionDefinition< 'repeatImage', number, Arguments, - Render + Promise> > { const { help, args: argHelp } = getFunctionHelp().repeatImage; - return { name: 'repeatImage', aliases: [], @@ -51,7 +50,7 @@ export function repeatImage(): ExpressionFunctionDefinition< image: { types: ['string', 'null'], help: argHelp.image, - default: elasticOutline, + default: null, }, max: { types: ['number'], @@ -64,7 +63,12 @@ export function repeatImage(): ExpressionFunctionDefinition< help: argHelp.size, }, }, - fn: (count, args) => { + fn: async (count, args) => { + const { elasticOutline } = await getElasticOutline(); + if (args.image === null) { + args.image = elasticOutline; + } + return { type: 'render', as: 'repeatImage', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js index 0ef832d97327..987096cc0d3d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js @@ -10,6 +10,7 @@ import { rounddate } from './rounddate'; describe('rounddate', () => { const fn = functionWrapper(rounddate); + const date = new Date('2011-10-31T00:00:00.000Z').valueOf(); it('returns date in ms from date in ms or ISO8601 string', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js index c6f592889c99..ffa1557d2b54 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js @@ -12,6 +12,7 @@ import { switchFn } from './switch'; describe('switch', () => { const fn = functionWrapper(switchFn); + const getter = (value) => () => of(value); const mockCases = [ { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js index 420489754d20..75744f91ccb5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js @@ -11,6 +11,7 @@ import { tail } from './tail'; describe('tail', () => { const fn = functionWrapper(tail); + const lastIndex = testTable.rows.length - 1; it('returns a datatable with the last N rows of the context', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js index f45ec981b1a8..38b0d1bff606 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js @@ -15,7 +15,7 @@ const errors = getFunctionErrors().timefilter; let clock = null; -beforeEach(function () { +beforeEach(async function () { clock = sinon.useFakeTimers(); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot index 9b97ae1fdacb..5e6b8214c3c4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot @@ -1,5 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Storyshots enderers/repeatImage default 1`] = ` +
+ +
+`; + exports[`Storyshots renderers/repeatImage default 1`] = `
{ +const Renderer = ({ elasticLogo }: { elasticLogo: string }) => { const config = { type: 'image' as 'image', mode: 'cover', @@ -19,4 +20,10 @@ storiesOf('renderers/image', module).add('default', () => { }; return ; -}); +}; + +storiesOf('renderers/image', module).add( + 'default', + (_, props) => , + { decorators: [waitFor(getElasticLogo())] } +); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx index ed2706389d83..0052b9139aae 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx @@ -8,13 +8,20 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { repeatImage } from '../repeat_image'; -import { Render } from './render'; import { - elasticLogo, - elasticOutline, + getElasticLogo, + getElasticOutline, } from '../../../../../../src/plugins/presentation_util/common/lib'; +import { waitFor } from '../../../../../../src/plugins/presentation_util/public/__stories__'; +import { Render } from './render'; -storiesOf('renderers/repeatImage', module).add('default', () => { +const Renderer = ({ + elasticLogo, + elasticOutline, +}: { + elasticLogo: string; + elasticOutline: string; +}) => { const config = { count: 42, image: elasticLogo, @@ -24,4 +31,12 @@ storiesOf('renderers/repeatImage', module).add('default', () => { }; return ; -}); +}; + +storiesOf('enderers/repeatImage', module).add( + 'default', + (_, props) => ( + + ), + { decorators: [waitFor(getElasticLogo()), waitFor(getElasticOutline())] } +); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx index 86e9daed105d..78e3ecb7a4c9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx @@ -7,7 +7,10 @@ import ReactDOM from 'react-dom'; import React from 'react'; -import { elasticLogo, isValidUrl } from '../../../../../src/plugins/presentation_util/common/lib'; +import { + getElasticLogo, + isValidUrl, +} from '../../../../../src/plugins/presentation_util/common/lib'; import { Return as Arguments } from '../functions/common/image'; import { RendererStrings } from '../../i18n'; import { RendererFactory } from '../../types'; @@ -19,7 +22,8 @@ export const image: RendererFactory = () => ({ displayName: strings.getDisplayName(), help: strings.getHelpDescription(), reuseDomNode: true, - render(domNode, config, handlers) { + render: async (domNode, config, handlers) => { + const { elasticLogo } = await getElasticLogo(); const dataurl = isValidUrl(config.dataurl) ? config.dataurl : elasticLogo; const style = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts index 149a88768341..b7a94c2089d8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts @@ -8,7 +8,7 @@ import $ from 'jquery'; import { times } from 'lodash'; import { - elasticOutline, + getElasticOutline, isValidUrl, } from '../../../../../src/plugins/presentation_util/common/lib'; import { RendererStrings, ErrorStrings } from '../../i18n'; @@ -23,10 +23,14 @@ export const repeatImage: RendererFactory = () => ({ displayName: strings.getDisplayName(), help: strings.getHelpDescription(), reuseDomNode: true, - render(domNode, config, handlers) { + render: async (domNode, config, handlers) => { + let image = config.image; + if (!isValidUrl(config.image)) { + image = (await getElasticOutline()).elasticOutline; + } const settings = { ...config, - image: isValidUrl(config.image) ? config.image : elasticOutline, + image, emptyImage: config.emptyImage || '', }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js index 480d8ea364c4..4597826c031a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js @@ -12,7 +12,7 @@ import { get } from 'lodash'; import { AssetPicker } from '../../../../public/components/asset_picker'; import { encode, - elasticOutline, + getElasticOutline, isValidHttpUrl, resolveFromArgs, } from '../../../../../../../src/plugins/presentation_util/public'; @@ -168,13 +168,16 @@ class ImageUpload extends React.Component { } } -export const imageUpload = () => ({ - name: 'imageUpload', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - resolveArgValue: true, - template: templateFromReactComponent(ImageUpload), - resolve({ args }) { - return { dataurl: resolveFromArgs(args, elasticOutline) }; - }, -}); +export const imageUpload = () => { + return { + name: 'imageUpload', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + resolveArgValue: true, + template: templateFromReactComponent(ImageUpload), + resolve: async ({ args }) => { + const { elasticOutline } = await getElasticOutline(); + return { dataurl: resolveFromArgs(args, elasticOutline) }; + }, + }; +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js index f974667b7fad..c3855ad31e3f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js @@ -6,38 +6,41 @@ */ import { - elasticLogo, + getElasticLogo, resolveFromArgs, } from '../../../../../../src/plugins/presentation_util/common/lib'; import { ViewStrings } from '../../../i18n'; const { Image: strings } = ViewStrings; -export const image = () => ({ - name: 'image', - displayName: strings.getDisplayName(), - modelArgs: [], - requiresContext: false, - args: [ - { - name: 'dataurl', - argType: 'imageUpload', - resolve({ args }) { - return { dataurl: resolveFromArgs(args, elasticLogo) }; +export const image = () => { + return { + name: 'image', + displayName: strings.getDisplayName(), + modelArgs: [], + requiresContext: false, + args: [ + { + name: 'dataurl', + argType: 'imageUpload', + resolve: async ({ args }) => { + const { elasticLogo } = await getElasticLogo(); + return { dataurl: resolveFromArgs(args, elasticLogo) }; + }, }, - }, - { - name: 'mode', - displayName: strings.getModeDisplayName(), - help: strings.getModeHelp(), - argType: 'select', - options: { - choices: [ - { value: 'contain', name: strings.getContainMode() }, - { value: 'cover', name: strings.getCoverMode() }, - { value: 'stretch', name: strings.getStretchMode() }, - ], + { + name: 'mode', + displayName: strings.getModeDisplayName(), + help: strings.getModeHelp(), + argType: 'select', + options: { + choices: [ + { value: 'contain', name: strings.getContainMode() }, + { value: 'cover', name: strings.getCoverMode() }, + { value: 'stretch', name: strings.getStretchMode() }, + ], + }, }, - }, - ], -}); + ], + }; +}; diff --git a/x-pack/plugins/canvas/i18n/constants.ts b/x-pack/plugins/canvas/i18n/constants.ts index 74646e140a20..2f192fd3e2d5 100644 --- a/x-pack/plugins/canvas/i18n/constants.ts +++ b/x-pack/plugins/canvas/i18n/constants.ts @@ -51,3 +51,6 @@ export const TYPE_STRING = '`string`'; export const URL = 'URL'; export const UTC = 'UTC'; export const ZIP = 'ZIP'; +export const IMAGE_MODE_CONTAIN = 'contain'; +export const IMAGE_MODE_COVER = 'cover'; +export const IMAGE_MODE_STRETCH = 'stretch'; diff --git a/x-pack/plugins/canvas/i18n/functions/dict/image.ts b/x-pack/plugins/canvas/i18n/functions/dict/image.ts index 5e643cf8de6e..b619d550f9ef 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/image.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/image.ts @@ -6,10 +6,16 @@ */ import { i18n } from '@kbn/i18n'; -import { image, ImageMode } from '../../../canvas_plugin_src/functions/common/image'; +import { image } from '../../../canvas_plugin_src/functions/common/image'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { URL, BASE64 } from '../../constants'; +import { + URL, + BASE64, + IMAGE_MODE_CONTAIN, + IMAGE_MODE_COVER, + IMAGE_MODE_STRETCH, +} from '../../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.imageHelpText', { @@ -35,9 +41,9 @@ export const help: FunctionHelp> = { '{cover} fills the container with the image, cropping from the sides or bottom as needed. ' + '{stretch} resizes the height and width of the image to 100% of the container.', values: { - contain: `\`"${ImageMode.CONTAIN}"\``, - cover: `\`"${ImageMode.COVER}"\``, - stretch: `\`"${ImageMode.STRETCH}"\``, + contain: `\`"${IMAGE_MODE_CONTAIN}"\``, + cover: `\`"${IMAGE_MODE_COVER}"\``, + stretch: `\`"${IMAGE_MODE_STRETCH}"\``, }, }), }, @@ -49,9 +55,9 @@ export const errors = { i18n.translate('xpack.canvas.functions.image.invalidImageModeErrorMessage', { defaultMessage: '"mode" must be "{contain}", "{cover}", or "{stretch}"', values: { - contain: ImageMode.CONTAIN, - cover: ImageMode.COVER, - stretch: ImageMode.STRETCH, + contain: IMAGE_MODE_CONTAIN, + cover: IMAGE_MODE_COVER, + stretch: IMAGE_MODE_STRETCH, }, }) ), diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx index 93574270757f..a072579be2e8 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { CustomElementModal } from '../custom_element_modal'; -import { elasticLogo } from '../../../../../../../src/plugins/presentation_util/public'; +import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__'; +import { getElasticLogo } from '../../../../../../../src/plugins/presentation_util/public'; storiesOf('components/Elements/CustomElementModal', module) .add('with title', () => ( @@ -36,11 +37,15 @@ storiesOf('components/Elements/CustomElementModal', module) onSave={action('onSave')} /> )) - .add('with image', () => ( - - )); + .add( + 'with image', + (_, props) => ( + + ), + { decorators: [waitFor(getElasticLogo())] } + ); diff --git a/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx b/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx index 4c68f185b196..74cd86c8d17d 100644 --- a/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx +++ b/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { ElementCard } from '../element_card'; -import { elasticLogo } from '../../../../../../../src/plugins/presentation_util/public'; +import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__'; +import { getElasticLogo } from '../../../../../../../src/plugins/presentation_util/public'; storiesOf('components/Elements/ElementCard', module) .addDecorator((story) => ( @@ -27,13 +28,17 @@ storiesOf('components/Elements/ElementCard', module) description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis." /> )) - .add('with image', () => ( - - )) + .add( + 'with image', + (_, props) => ( + + ), + { decorators: [waitFor(getElasticLogo())] } + ) .add('with tags', () => ( = ({ functionRegistry }) => { const functionDefinitions = Object.values(functionRegistry); const copyDocs = () => { - copy(generateFunctionReference(functionDefinitions)); + const functionRefs = generateFunctionReference(functionDefinitions); + copy(functionRefs); notifyService.success( `Please paste updated docs into '/kibana/docs/canvas/canvas-function-reference.asciidoc' and commit your changes.`, { title: 'Copied function docs to clipboard' } diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx index 3c68afb2bcec..f267b48028f7 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -104,7 +104,7 @@ export const RenderWithFn: FC = ({ try { render(); firstRender.current = false; - } catch (err) { + } catch (err: any) { onError(err, { title: strings.getRenderErrorMessage(functionName) }); } }, [domNode, functionName, onError, render, resetRenderTarget, reuseNode]); diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx index 769078da972f..3f0611479c3b 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx @@ -8,8 +8,9 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; +import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__'; import { ElementGrid } from '../element_grid'; -import { testCustomElements } from './fixtures/test_elements'; +import { getTestCustomElements } from './fixtures/test_elements'; storiesOf('components/SavedElementsModal/ElementGrid', module) .addDecorator((story) => ( @@ -21,20 +22,28 @@ storiesOf('components/SavedElementsModal/ElementGrid', module) {story()}
)) - .add('default', () => ( - - )) - .add('with text filter', () => ( - - )); + .add( + 'default', + (_, props) => ( + + ), + { decorators: [waitFor(getTestCustomElements())] } + ) + .add( + 'with text filter', + (_, props) => ( + + ), + { decorators: [waitFor(getTestCustomElements())] } + ); diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx index ef48b9815062..854bf9c685ad 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx @@ -5,32 +5,36 @@ * 2.0. */ -import { elasticLogo } from '../../../../../../../../src/plugins/presentation_util/public'; +import { getElasticLogo } from '../../../../../../../../src/plugins/presentation_util/public'; -export const testCustomElements = [ - { - id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', - name: 'customElement1', - displayName: 'Custom Element 1', - help: 'sample description', - image: elasticLogo, - content: `{\"selectedNodes\":[{\"id\":\"element-3383b40a-de5d-4efb-8719-f4d8cffbfa74\",\"position\":{\"left\":142,\"top\":146,\"width\":700,\"height\":300,\"angle\":0,\"parent\":null,\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| pointseries x=\\\"project\\\" y=\\\"sum(price)\\\" color=\\\"state\\\" size=\\\"size(username)\\\"\\n| plot defaultStyle={seriesStyle points=5 fill=1}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"pointseries\",\"arguments\":{\"x\":[\"project\"],\"y\":[\"sum(price)\"],\"color\":[\"state\"],\"size\":[\"size(username)\"]}},{\"type\":\"function\",\"function\":\"plot\",\"arguments\":{\"defaultStyle\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"seriesStyle\",\"arguments\":{\"points\":[5],\"fill\":[1]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, - }, - { - id: 'custom-element-b22d8d10-6116-46fb-9b46-c3f3340d3aaa', - name: 'customElement2', - displayName: 'Custom Element 2', - help: 'Aenean eu justo auctor, placerat felis non, scelerisque dolor. ', - image: elasticLogo, - content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, - }, - { - id: 'custom-element-', - name: 'customElement3', - displayName: 'Custom Element 3', - help: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.', - image: elasticLogo, - content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, - }, -]; +export const getTestCustomElements = async () => { + const { elasticLogo } = await getElasticLogo(); + const testCustomElements = [ + { + id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', + name: 'customElement1', + displayName: 'Custom Element 1', + help: 'sample description', + image: elasticLogo, + content: `{\"selectedNodes\":[{\"id\":\"element-3383b40a-de5d-4efb-8719-f4d8cffbfa74\",\"position\":{\"left\":142,\"top\":146,\"width\":700,\"height\":300,\"angle\":0,\"parent\":null,\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| pointseries x=\\\"project\\\" y=\\\"sum(price)\\\" color=\\\"state\\\" size=\\\"size(username)\\\"\\n| plot defaultStyle={seriesStyle points=5 fill=1}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"pointseries\",\"arguments\":{\"x\":[\"project\"],\"y\":[\"sum(price)\"],\"color\":[\"state\"],\"size\":[\"size(username)\"]}},{\"type\":\"function\",\"function\":\"plot\",\"arguments\":{\"defaultStyle\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"seriesStyle\",\"arguments\":{\"points\":[5],\"fill\":[1]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-b22d8d10-6116-46fb-9b46-c3f3340d3aaa', + name: 'customElement2', + displayName: 'Custom Element 2', + help: 'Aenean eu justo auctor, placerat felis non, scelerisque dolor. ', + image: elasticLogo, + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-', + name: 'customElement3', + displayName: 'Custom Element 3', + help: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.', + image: elasticLogo, + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"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\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + ]; + return { testCustomElements }; +}; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx index d5c9af6ded8d..086a4be14021 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx @@ -8,8 +8,9 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; +import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__'; import { SavedElementsModal } from '../saved_elements_modal.component'; -import { testCustomElements } from './fixtures/test_elements'; +import { getTestCustomElements } from './fixtures/test_elements'; import { CustomElement } from '../../../../types'; storiesOf('components/SavedElementsModal', module) @@ -25,27 +26,35 @@ storiesOf('components/SavedElementsModal', module) removeCustomElement={action('removeCustomElement')} /> )) - .add('with custom elements', () => ( - - )) - .add('with text filter', () => ( - - )); + .add( + 'with custom elements', + (_, props) => ( + + ), + { decorators: [waitFor(getTestCustomElements())] } + ) + .add( + 'with text filter', + (_, props) => ( + + ), + { decorators: [waitFor(getTestCustomElements())] } + ); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx index 284749340e44..a35b83bad3e7 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx @@ -75,12 +75,7 @@ export const CustomInterval = ({ gutterSize, buttonSize, onSubmit, defaultValue - + {strings.getButtonLabel()} diff --git a/x-pack/plugins/canvas/public/functions/pie.test.js b/x-pack/plugins/canvas/public/functions/pie.test.js index 5e35cc3bf523..f1ce8786f2c6 100644 --- a/x-pack/plugins/canvas/public/functions/pie.test.js +++ b/x-pack/plugins/canvas/public/functions/pie.test.js @@ -30,9 +30,9 @@ describe('pie', () => { }); describe('data', () => { - const result = fn(testPie).value.data; - it('has one series per unique label', () => { + const result = fn(testPie).value.data; + const uniqueLabels = testPie.rows.reduce( (unique, series) => !unique.includes(series.color) ? unique.concat([series.color]) : unique, @@ -44,6 +44,8 @@ describe('pie', () => { }); it('populates the data of the plot with points from the pointseries', () => { + const result = fn(testPie).value.data; + expect(result[0].data).toEqual([202]); expect(result[1].data).toEqual([67]); expect(result[2].data).toEqual([311]); diff --git a/x-pack/plugins/canvas/public/functions/plot.test.js b/x-pack/plugins/canvas/public/functions/plot.test.js index 8dd2470ea17d..a2eef889aa07 100644 --- a/x-pack/plugins/canvas/public/functions/plot.test.js +++ b/x-pack/plugins/canvas/public/functions/plot.test.js @@ -33,12 +33,15 @@ describe('plot', () => { }); describe('data', () => { - const result = fn(testPlot).value.data; it('is sorted by the series labels', () => { + const result = fn(testPlot).value.data; + expect(result.every((val, i) => (!!i ? val.label >= result[i - 1].label : true))).toBe(true); }); it('has one series per unique label', () => { + const result = fn(testPlot).value.data; + const uniqueLabels = testPlot.rows .reduce( (unique, series) => @@ -52,6 +55,8 @@ describe('plot', () => { }); it('populates the data of the plot with points from the pointseries', () => { + const result = fn(testPlot).value.data; + expect(result[0].data).toEqual([ [1517842800950, 605, { size: 100, text: 605 }], [1517929200950, 583, { size: 200, text: 583 }], @@ -118,6 +123,7 @@ describe('plot', () => { describe('palette', () => { it('sets the color palette', () => { const mockedColors = jest.fn(() => ['#FFFFFF', '#888888', '#000000']); + const mockedFn = functionWrapper( plotFunctionFactory({ get: () => ({ diff --git a/x-pack/plugins/canvas/public/lib/monaco_language_def.ts b/x-pack/plugins/canvas/public/lib/monaco_language_def.ts index f370ab7a9943..12dc385a4147 100644 --- a/x-pack/plugins/canvas/public/lib/monaco_language_def.ts +++ b/x-pack/plugins/canvas/public/lib/monaco_language_def.ts @@ -97,7 +97,6 @@ export const language: Language = { export function registerLanguage(functions: ExpressionFunction[]) { language.keywords = functions.map((fn) => fn.name); - monaco.languages.register({ id: LANGUAGE_ID }); monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, language); } diff --git a/x-pack/plugins/canvas/public/plugin_api.ts b/x-pack/plugins/canvas/public/plugin_api.ts index 8f39f2d990d0..55a7390437c2 100644 --- a/x-pack/plugins/canvas/public/plugin_api.ts +++ b/x-pack/plugins/canvas/public/plugin_api.ts @@ -22,7 +22,9 @@ export interface CanvasApi { addArgumentUIs: AddToRegistry; addDatasourceUIs: AddToRegistry; addElements: AddToRegistry; - addFunctions: AddSpecsToRegistry<() => AnyExpressionFunctionDefinition>; + addFunctions: AddSpecsToRegistry< + (() => AnyExpressionFunctionDefinition) | AnyExpressionFunctionDefinition + >; addModelUIs: AddToRegistry; addRenderers: AddSpecsToRegistry; addTagUIs: AddToRegistry; diff --git a/x-pack/plugins/canvas/public/services/legacy/expressions.ts b/x-pack/plugins/canvas/public/services/legacy/expressions.ts index 99915cad745e..b48172172e3c 100644 --- a/x-pack/plugins/canvas/public/services/legacy/expressions.ts +++ b/x-pack/plugins/canvas/public/services/legacy/expressions.ts @@ -42,6 +42,7 @@ export const expressionsServiceFactory: CanvasServiceFactory return batchedFunction({ functionName, args, context: serialize(input) }); }, }); + expressions.registerFunction(fn); }); })(); diff --git a/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts index bd1076ab0bf8..1093bb745213 100644 --- a/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts +++ b/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts @@ -17,7 +17,9 @@ const setup = expressionsPlugin.setup(placeholder); export const expressionsService: ExpressionsService = setup.fork(); -functionDefinitions.forEach((fn) => expressionsService.registerFunction(fn)); -renderFunctions.forEach((fn) => { - expressionsService.registerRenderer((fn as unknown) as AnyExpressionRenderDefinition); -}); +export function setupExpressionsService() { + functionDefinitions.forEach((fn) => expressionsService.registerFunction(fn)); + renderFunctions.forEach((fn) => { + expressionsService.registerRenderer((fn as unknown) as AnyExpressionRenderDefinition); + }); +} diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap index 075c0cd38675..988aba48d976 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap @@ -324,6 +324,7 @@ exports[` can navigate Autoplay Settings 2`] = `

} @@ -61,7 +61,7 @@ export const NoEnrichmentsPanel: React.FC<{ } /> ); - } else if (existingEnrichmentsCount === 0) { + } else if (!isIndicatorMatchesPresent) { return ( <> @@ -75,7 +75,7 @@ export const NoEnrichmentsPanel: React.FC<{ /> ); - } else if (investigationEnrichmentsCount === 0) { + } else if (!isInvestigationTimeEnrichmentsPresent) { return ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 7074212dcdb4..668c6ffb723a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -113,15 +113,14 @@ const EventDetailsComponent: React.FC = ({ loading: enrichmentsLoading, result: enrichmentsResponse, } = useInvestigationTimeEnrichment(eventFields); - const investigationEnrichments = useMemo(() => enrichmentsResponse?.enrichments ?? [], [ - enrichmentsResponse?.enrichments, - ]); + const allEnrichments = useMemo(() => { if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { return existingEnrichments; } return filterDuplicateEnrichments([...existingEnrichments, ...enrichmentsResponse.enrichments]); }, [enrichmentsLoading, enrichmentsResponse, existingEnrichments]); + const enrichmentCount = allEnrichments.length; const summaryTab: EventViewTab | undefined = useMemo( @@ -184,21 +183,16 @@ const EventDetailsComponent: React.FC = ({ <> existingEnrichments.length + } + isIndicatorMatchesPresent={existingEnrichments.length > 0} /> ), } : undefined, - [ - allEnrichments, - enrichmentCount, - enrichmentsLoading, - existingEnrichments.length, - investigationEnrichments.length, - isAlert, - ] + [allEnrichments, enrichmentCount, enrichmentsLoading, existingEnrichments.length, isAlert] ); const tableTab = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 32eb4baad505..bf6a94c53b47 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -26,8 +26,9 @@ import { getProcessCodeSignature, retrieveAlertOsTypes, filterIndexPatterns, + getCodeSignatureValue, } from './helpers'; -import { AlertData } from './types'; +import { AlertData, Flattened } from './types'; import { ListOperatorTypeEnum as OperatorTypeEnum, EntriesArray, @@ -41,6 +42,7 @@ import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/ import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock'; import { IFieldType, IIndexPattern } from 'src/plugins/data/common'; +import { CodeSignature } from '../../../../common/ecs/file'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('123'), @@ -340,6 +342,17 @@ describe('Exception helpers', () => { }); }); + describe('#getCodeSignatureValue', () => { + test('it should return empty string if code_signature nested value are undefined', () => { + // Using the unsafe casting because with our types this shouldn't be possible but there have been issues with old data having undefined values in these fields + const payload = ([{ trusted: undefined, subject_name: undefined }] as unknown) as Flattened< + CodeSignature[] + >; + const result = getCodeSignatureValue(payload); + expect(result).toEqual([{ trusted: '', subjectName: '' }]); + }); + }); + describe('#entryHasNonEcsType', () => { const mockEcsIndexPattern = { title: 'testIndex', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3c8652637a99..f8260062f697 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -325,8 +325,8 @@ export const getCodeSignatureValue = ( if (Array.isArray(codeSignature) && codeSignature.length > 0) { return codeSignature.map((signature) => { return { - subjectName: signature.subject_name ?? '', - trusted: signature.trusted.toString() ?? '', + subjectName: signature?.subject_name ?? '', + trusted: signature?.trusted?.toString() ?? '', }; }); } else { diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx new file mode 100644 index 000000000000..fba9dd734600 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx @@ -0,0 +1,85 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { StatefulTopN } from '../../top_n'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { SourcererScopeName } from '../../../store/sourcerer/model'; +import { useSourcererScope } from '../../../containers/sourcerer'; +import { TooltipWithKeyboardShortcut } from '../../accessibility'; +import { getAdditionalScreenReaderOnlyContext } from '../utils'; +import { SHOW_TOP_N_KEYBOARD_SHORTCUT } from '../keyboard_shortcut_constants'; + +const SHOW_TOP = (fieldName: string) => + i18n.translate('xpack.securitySolution.hoverActions.showTopTooltip', { + values: { fieldName }, + defaultMessage: `Show top {fieldName}`, + }); + +interface Props { + field: string; + onClick: () => void; + onFilterAdded?: () => void; + ownFocus: boolean; + showTopN: boolean; + timelineId?: string | null; + value?: string[] | string | null; +} + +export const ShowTopNButton: React.FC = React.memo( + ({ field, onClick, onFilterAdded, ownFocus, showTopN, timelineId, value }) => { + const activeScope: SourcererScopeName = + timelineId === TimelineId.active + ? SourcererScopeName.timeline + : timelineId != null && + [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes( + timelineId as TimelineId + ) + ? SourcererScopeName.detections + : SourcererScopeName.default; + const { browserFields, indexPattern } = useSourcererScope(activeScope); + + return showTopN ? ( + + ) : ( + + } + > + + + ); + } +); + +ShowTopNButton.displayName = 'ShowTopNButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx new file mode 100644 index 000000000000..bd5d78fd4e85 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx @@ -0,0 +1,349 @@ +/* + * 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 { EuiFocusTrap, EuiScreenReaderOnly } from '@elastic/eui'; +import React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import { DraggableId } from 'react-beautiful-dnd'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { getAllFieldsByName } from '../../containers/source'; +import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard'; +import { useKibana } from '../../lib/kibana'; +import { allowTopN } from './utils'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { ColumnHeaderOptions, TimelineId } from '../../../../common/types/timeline'; +import { SourcererScopeName } from '../../store/sourcerer/model'; +import { useSourcererScope } from '../../containers/sourcerer'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { stopPropagationAndPreventDefault } from '../../../../../timelines/public'; +import { ShowTopNButton } from './actions/show_top_n'; +import { SHOW_TOP_N_KEYBOARD_SHORTCUT } from './keyboard_shortcut_constants'; + +export const YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS = (fieldName: string) => + i18n.translate( + 'xpack.securitySolution.dragAndDrop.youAreInADialogContainingOptionsScreenReaderOnly', + { + values: { fieldName }, + defaultMessage: `You are in a dialog, containing options for field {fieldName}. Press tab to navigate options. Press escape to exit.`, + } + ); + +export const AdditionalContent = styled.div` + padding: 2px; +`; + +AdditionalContent.displayName = 'AdditionalContent'; + +const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>` + padding: ${(props) => (props.$showTopN ? 'none' : props.theme.eui.paddingSizes.s)}; + + &:focus-within { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + } + + &:hover { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + } + + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + // TODO: Using this logic from discover + /* @include euiBreakpoint('m', 'l', 'xl') { + opacity: 0; + } */ + opacity: 0; + + &:focus { + opacity: 1; + } + } +`; + +interface Props { + additionalContent?: React.ReactNode; + dataType?: string; + draggableId?: DraggableId; + field: string; + isObjectArray: boolean; + goGetTimelineId?: (args: boolean) => void; + onFilterAdded?: () => void; + ownFocus: boolean; + showTopN: boolean; + timelineId?: string | null; + toggleColumn?: (column: ColumnHeaderOptions) => void; + toggleTopN: () => void; + value?: string[] | string | null; +} + +/** Returns a value for the `disabled` prop of `EuiFocusTrap` */ +const isFocusTrapDisabled = ({ + ownFocus, + showTopN, +}: { + ownFocus: boolean; + showTopN: boolean; +}): boolean => { + if (showTopN) { + return false; // we *always* want to trap focus when showing Top N + } + + return !ownFocus; +}; + +export const HoverActions: React.FC = React.memo( + ({ + additionalContent = null, + dataType, + draggableId, + field, + goGetTimelineId, + isObjectArray, + onFilterAdded, + ownFocus, + showTopN, + timelineId, + toggleColumn, + toggleTopN, + value, + }) => { + const kibana = useKibana(); + const { timelines } = kibana.services; + // Common actions used by the alert table and alert flyout + const { + addToTimeline: { + AddToTimelineButton, + keyboardShortcut: addToTimelineKeyboardShortcut, + useGetHandleStartDragToTimeline, + }, + columnToggle: { + ColumnToggleButton, + columnToggleFn, + keyboardShortcut: columnToggleKeyboardShortcut, + }, + copy: { CopyButton, keyboardShortcut: copyKeyboardShortcut }, + filterForValue: { + FilterForValueButton, + filterForValueFn, + keyboardShortcut: filterForValueKeyboardShortcut, + }, + filterOutValue: { + FilterOutValueButton, + filterOutValueFn, + keyboardShortcut: filterOutValueKeyboardShortcut, + }, + } = timelines.getHoverActions(); + + const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ + kibana.services.data.query.filterManager, + ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) => + getManageTimeline(state, timelineId ?? '') + ); + const filterManager = useMemo( + () => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup), + [timelineId, activeFilterMananager, filterManagerBackup] + ); + + // Regarding data from useManageTimeline: + // * `indexToAdd`, which enables the alerts index to be appended to + // the `indexPattern` returned by `useWithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the alerts index + // to the index pattern. + const activeScope: SourcererScopeName = + timelineId === TimelineId.active + ? SourcererScopeName.timeline + : timelineId != null && + [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes( + timelineId as TimelineId + ) + ? SourcererScopeName.detections + : SourcererScopeName.default; + const { browserFields } = useSourcererScope(activeScope); + + const handleStartDragToTimeline = useGetHandleStartDragToTimeline({ draggableId, field }); + + const handleFilterForValue = useCallback( + () => filterForValueFn({ field, value, filterManager, onFilterAdded }), + [filterForValueFn, field, value, filterManager, onFilterAdded] + ); + + const handleFilterOutValue = useCallback( + () => filterOutValueFn({ field, value, filterManager, onFilterAdded }), + [filterOutValueFn, field, value, filterManager, onFilterAdded] + ); + + const handleToggleColumn = useCallback( + () => (toggleColumn ? columnToggleFn({ toggleColumn, field }) : null), + [columnToggleFn, field, toggleColumn] + ); + + const isInit = useRef(true); + const defaultFocusedButtonRef = useRef(null); + const panelRef = useRef(null); + + useEffect(() => { + if (isInit.current && goGetTimelineId != null && timelineId == null) { + isInit.current = false; + goGetTimelineId(true); + } + }, [goGetTimelineId, timelineId]); + + useEffect(() => { + if (ownFocus) { + setTimeout(() => { + defaultFocusedButtonRef.current?.focus(); + }, 0); + } + }, [ownFocus]); + + const onKeyDown = useCallback( + (keyboardEvent: React.KeyboardEvent) => { + if (!ownFocus) { + return; + } + switch (keyboardEvent.key) { + case addToTimelineKeyboardShortcut: + stopPropagationAndPreventDefault(keyboardEvent); + handleStartDragToTimeline(); + break; + case columnToggleKeyboardShortcut: + stopPropagationAndPreventDefault(keyboardEvent); + handleToggleColumn(); + break; + case copyKeyboardShortcut: + stopPropagationAndPreventDefault(keyboardEvent); + const copyToClipboardButton = panelRef.current?.querySelector( + `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}` + ); + if (copyToClipboardButton != null) { + copyToClipboardButton.click(); + } + break; + case filterForValueKeyboardShortcut: + stopPropagationAndPreventDefault(keyboardEvent); + handleFilterForValue(); + break; + case filterOutValueKeyboardShortcut: + stopPropagationAndPreventDefault(keyboardEvent); + handleFilterOutValue(); + break; + case SHOW_TOP_N_KEYBOARD_SHORTCUT: + stopPropagationAndPreventDefault(keyboardEvent); + toggleTopN(); + break; + case 'Enter': + break; + case 'Escape': + stopPropagationAndPreventDefault(keyboardEvent); + break; + default: + break; + } + }, + + [ + addToTimelineKeyboardShortcut, + columnToggleKeyboardShortcut, + copyKeyboardShortcut, + filterForValueKeyboardShortcut, + filterOutValueKeyboardShortcut, + handleFilterForValue, + handleFilterOutValue, + handleStartDragToTimeline, + handleToggleColumn, + ownFocus, + toggleTopN, + ] + ); + + const showFilters = !showTopN && value != null; + + return ( + + + +

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}

+
+ + {additionalContent != null && {additionalContent}} + + {showFilters && ( + <> + + + + )} + {toggleColumn && ( + + )} + + {showFilters && draggableId != null && ( + + )} + {allowTopN({ + browserField: getAllFieldsByName(browserFields)[field], + fieldName: field, + }) && ( + + )} + {!showTopN && ( + + )} +
+
+ ); + } +); + +HoverActions.displayName = 'HoverActions'; diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/keyboard_shortcut_constants.ts b/x-pack/plugins/security_solution/public/common/components/hover_actions/keyboard_shortcut_constants.ts new file mode 100644 index 000000000000..cef4c896e39f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/keyboard_shortcut_constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const SHOW_TOP_N_KEYBOARD_SHORTCUT = 't'; diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/utils.ts b/x-pack/plugins/security_solution/public/common/components/hover_actions/utils.ts new file mode 100644 index 000000000000..9a6b0838c668 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/utils.ts @@ -0,0 +1,117 @@ +/* + * 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 { BrowserField } from '../../containers/source'; + +export const getAdditionalScreenReaderOnlyContext = ({ + field, + value, +}: { + field: string; + value?: string[] | string | null; +}): string => { + if (value == null) { + return field; + } + + return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`; +}; + +export const allowTopN = ({ + browserField, + fieldName, +}: { + browserField: Partial | undefined; + fieldName: string; +}): boolean => { + const isAggregatable = browserField?.aggregatable ?? false; + const fieldType = browserField?.type ?? ''; + const isAllowedType = [ + 'boolean', + 'geo-point', + 'geo-shape', + 'ip', + 'keyword', + 'number', + 'numeric', + 'string', + ].includes(fieldType); + + // TODO: remove this explicit allowlist when the ECS documentation includes alerts + const isAllowlistedNonBrowserField = [ + 'signal.ancestors.depth', + 'signal.ancestors.id', + 'signal.ancestors.rule', + 'signal.ancestors.type', + 'signal.original_event.action', + 'signal.original_event.category', + 'signal.original_event.code', + 'signal.original_event.created', + 'signal.original_event.dataset', + 'signal.original_event.duration', + 'signal.original_event.end', + 'signal.original_event.hash', + 'signal.original_event.id', + 'signal.original_event.kind', + 'signal.original_event.module', + 'signal.original_event.original', + 'signal.original_event.outcome', + 'signal.original_event.provider', + 'signal.original_event.risk_score', + 'signal.original_event.risk_score_norm', + 'signal.original_event.sequence', + 'signal.original_event.severity', + 'signal.original_event.start', + 'signal.original_event.timezone', + 'signal.original_event.type', + 'signal.original_time', + 'signal.parent.depth', + 'signal.parent.id', + 'signal.parent.index', + 'signal.parent.rule', + 'signal.parent.type', + 'signal.rule.created_by', + 'signal.rule.description', + 'signal.rule.enabled', + 'signal.rule.false_positives', + 'signal.rule.filters', + 'signal.rule.from', + 'signal.rule.id', + 'signal.rule.immutable', + 'signal.rule.index', + 'signal.rule.interval', + 'signal.rule.language', + 'signal.rule.max_signals', + 'signal.rule.name', + 'signal.rule.note', + 'signal.rule.output_index', + 'signal.rule.query', + 'signal.rule.references', + 'signal.rule.risk_score', + 'signal.rule.rule_id', + 'signal.rule.saved_id', + 'signal.rule.severity', + 'signal.rule.size', + 'signal.rule.tags', + 'signal.rule.threat', + 'signal.rule.threat.tactic.id', + 'signal.rule.threat.tactic.name', + 'signal.rule.threat.tactic.reference', + 'signal.rule.threat.technique.id', + 'signal.rule.threat.technique.name', + 'signal.rule.threat.technique.reference', + 'signal.rule.timeline_id', + 'signal.rule.timeline_title', + 'signal.rule.to', + 'signal.rule.type', + 'signal.rule.updated_by', + 'signal.rule.version', + 'signal.status', + ].includes(fieldName); + + return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType); +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx index 678a0586f3c0..d213cb7ab873 100644 --- a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx @@ -6,6 +6,7 @@ */ import { EuiButtonIcon } from '@elastic/eui'; +import classNames from 'classnames'; import copy from 'copy-to-clipboard'; import React from 'react'; @@ -25,12 +26,20 @@ export type OnCopy = ({ interface Props { children?: JSX.Element; content: string | number; + isHoverAction?: boolean; onCopy?: OnCopy; titleSummary?: string; toastLifeTimeMs?: number; } -export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTimeMs }: Props) => { +export const Clipboard = ({ + children, + content, + isHoverAction, + onCopy, + titleSummary, + toastLifeTimeMs, +}: Props) => { const { addSuccess } = useAppToasts(); const onClick = (event: React.MouseEvent) => { event.preventDefault(); @@ -47,11 +56,15 @@ export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTi } }; + const className = classNames(COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME, { + // eslint-disable-next-line @typescript-eslint/naming-convention + securitySolution__hoverActionButton: isHoverAction, + }); + return ( (({ keyboardShortcut = '', text, titleSummary }) => ( +}>(({ isHoverAction, keyboardShortcut = '', text, titleSummary }) => ( } > - + )); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts index 5d6744de9dbe..4dbcd515db4c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts @@ -27,9 +27,14 @@ export const track: TrackFn = (type, event, count) => { }; export const initTelemetry = ( - { usageCollection }: Pick, + { + usageCollection, + telemetryManagementSection, + }: Pick, appId: string ) => { + telemetryManagementSection?.toggleSecuritySolutionExample(true); + _track = usageCollection?.reportUiCounter?.bind(null, appId) ?? noop; }; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx index da5216be7db3..38c352c43b0d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx @@ -41,7 +41,12 @@ export const CtiDisabledModuleComponent = () => { ); return ( - + ); }; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx index 9714c28cc58c..ffd0c8e69e76 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx @@ -144,4 +144,32 @@ describe('ThreatIntelPanelView', () => { `Showing: ${mockThreatIntelPanelViewProps.totalEventCount} indicators` ); }); + + it('renders inspect button by default', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.exists('[data-test-subj="inspect-icon-button"]')).toBe(true); + }); + + it('does not render inspect button if isInspectEnabled is false', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.exists('[data-test-subj="inspect-icon-button"]')).toBe(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx index 2add03788eea..6bd7bef20fcb 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -55,6 +55,7 @@ const RightSideLink = styled(EuiLink)` interface ThreatIntelPanelViewProps { buttonHref?: string; isDashboardPluginDisabled?: boolean; + isInspectEnabled?: boolean; listItems: CtiListItem[]; splitPanel?: JSX.Element; totalEventCount: number; @@ -77,6 +78,7 @@ const panelTitle = ( export const ThreatIntelPanelView: React.FC = ({ buttonHref = '', isDashboardPluginDisabled, + isInspectEnabled = true, listItems, splitPanel, totalEventCount, @@ -142,7 +144,11 @@ export const ThreatIntelPanelView: React.FC = ({ - + <>{button} {splitPanel} diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index a7cb9ee68891..137fef164150 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -91,6 +91,7 @@ export class Plugin implements IPlugin void; + +interface Props { + children?: JSX.Element; + content: string | number; + isHoverAction?: boolean; + onCopy?: OnCopy; + titleSummary?: string; + toastLifeTimeMs?: number; +} + +export const Clipboard = ({ + children, + content, + isHoverAction, + onCopy, + titleSummary, + toastLifeTimeMs, +}: Props) => { + const { addSuccess } = useAppToasts(); + const onClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const isSuccess = copy(`${content}`, { debug: true }); + + if (onCopy != null) { + onCopy({ content, isSuccess }); + } + + if (isSuccess) { + addSuccess(`${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, { toastLifeTimeMs }); + } + }; + + const className = classNames(COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME, { + // eslint-disable-next-line @typescript-eslint/naming-convention + securitySolution__hoverActionButton: isHoverAction, + }); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/timelines/public/components/clipboard/translations.ts b/x-pack/plugins/timelines/public/components/clipboard/translations.ts new file mode 100644 index 000000000000..a92c9656f3cf --- /dev/null +++ b/x-pack/plugins/timelines/public/components/clipboard/translations.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const COPY = i18n.translate('xpack.timelines.clipboard.copy', { + defaultMessage: 'Copy', +}); + +export const COPIED = i18n.translate('xpack.timelines.clipboard.copied', { + defaultMessage: 'Copied', +}); + +export const TO_THE_CLIPBOARD = i18n.translate('xpack.timelines.clipboard.to.the.clipboard', { + defaultMessage: 'to the clipboard', +}); + +export const COPY_TO_THE_CLIPBOARD = i18n.translate( + 'xpack.timelines.clipboard.copy.to.the.clipboard', + { + defaultMessage: 'Copy to the clipboard', + } +); diff --git a/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx new file mode 100644 index 000000000000..a62f52c27cf7 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiToolTip } from '@elastic/eui'; + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { TooltipWithKeyboardShortcut } from '../tooltip_with_keyboard_shortcut'; +import { Clipboard } from '.'; + +export const COPY_TO_CLIPBOARD = i18n.translate('xpack.timelines.copyToClipboardTooltip', { + defaultMessage: 'Copy to Clipboard', +}); + +/** + * Renders `children` with an adjacent icon that when clicked, copies `text` to + * the clipboard and displays a confirmation toast + */ +export const WithCopyToClipboard = React.memo<{ + isHoverAction?: boolean; + keyboardShortcut?: string; + showTooltip?: boolean; + text: string; + titleSummary?: string; +}>(({ isHoverAction, keyboardShortcut = '', showTooltip = false, text, titleSummary }) => { + return showTooltip ? ( + + } + > + + + ) : ( + + ); +}); + +WithCopyToClipboard.displayName = 'WithCopyToClipboard'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx new file mode 100644 index 000000000000..eb9c95f0998c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DraggableId } from 'react-beautiful-dnd'; +import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; +import { getAdditionalScreenReaderOnlyContext } from '../utils'; +import { useAddToTimeline } from '../../../hooks/use_add_to_timeline'; +import { HoverActionComponentProps } from './types'; + +const ADD_TO_TIMELINE = i18n.translate('xpack.timelines.hoverActions.addToTimeline', { + defaultMessage: 'Add to timeline investigation', +}); + +export const ADD_TO_TIMELINE_KEYBOARD_SHORTCUT = 'a'; + +export interface UseGetHandleStartDragToTimelineArgs { + field: string; + draggableId: DraggableId | undefined; +} + +export const useGetHandleStartDragToTimeline = ({ + field, + draggableId, +}: UseGetHandleStartDragToTimelineArgs): (() => void) => { + const { startDragToTimeline } = useAddToTimeline({ + draggableId, + fieldName: field, + }); + + const handleStartDragToTimeline = useCallback(() => { + startDragToTimeline(); + }, [startDragToTimeline]); + + return handleStartDragToTimeline; +}; + +export const AddToTimelineButton: React.FC = React.memo( + ({ field, onClick, ownFocus, showTooltip = false, value }) => { + return showTooltip ? ( + + } + > + + + ) : ( + + ); + } +); + +AddToTimelineButton.displayName = 'AddToTimelineButton'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx new file mode 100644 index 000000000000..52d8fb439526 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx @@ -0,0 +1,97 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; +import { getAdditionalScreenReaderOnlyContext } from '../utils'; +import { defaultColumnHeaderType } from '../../t_grid/body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../t_grid/body/constants'; +import { ColumnHeaderOptions } from '../../../../common/types/timeline'; +import { HoverActionComponentProps } from './types'; + +export const COLUMN_TOGGLE = (field: string) => + i18n.translate('xpack.timelines.hoverActions.columnToggleLabel', { + values: { field }, + defaultMessage: 'Toggle {field} column in table', + }); + +export const NESTED_COLUMN = (field: string) => + i18n.translate('xpack.timelines.hoverActions.nestedColumnToggleLabel', { + values: { field }, + defaultMessage: + 'The {field} field is an object, and is broken down into nested fields which can be added as columns', + }); + +export const COLUMN_TOGGLE_KEYBOARD_SHORTCUT = 'i'; + +export interface ColumnToggleFnArgs { + toggleColumn: (column: ColumnHeaderOptions) => void; + field: string; +} + +export const columnToggleFn = ({ toggleColumn, field }: ColumnToggleFnArgs): void => { + return toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }); +}; + +export interface ColumnToggleProps extends HoverActionComponentProps { + isDisabled: boolean; + isObjectArray: boolean; +} + +export const ColumnToggleButton: React.FC = React.memo( + ({ field, isDisabled, isObjectArray, onClick, ownFocus, showTooltip = false, value }) => { + const label = isObjectArray ? NESTED_COLUMN(field) : COLUMN_TOGGLE(field); + + return showTooltip ? ( + + } + > + + + ) : ( + + ); + } +); + +ColumnToggleButton.displayName = 'ColumnToggleButton'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx new file mode 100644 index 000000000000..33cc71e12c46 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard'; +import { HoverActionComponentProps } from './types'; + +export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', { + defaultMessage: 'Field', +}); + +export const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c'; + +export type CopyProps = Omit & { + isHoverAction?: boolean; +}; + +export const CopyButton: React.FC = React.memo( + ({ field, isHoverAction, ownFocus, value }) => { + return ( + + ); + } +); + +CopyButton.displayName = 'CopyButton'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx new file mode 100644 index 000000000000..f62d29c8b648 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx @@ -0,0 +1,81 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiButtonIconPropsForButton, EuiToolTip } from '@elastic/eui'; +import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; +import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; +import { HoverActionComponentProps, FilterValueFnArgs } from './types'; + +export const FILTER_FOR_VALUE = i18n.translate('xpack.timelines.hoverActions.filterForValue', { + defaultMessage: 'Filter for value', +}); +export const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f'; + +export const filterForValueFn = ({ + field, + value, + filterManager, + onFilterAdded, +}: FilterValueFnArgs): void => { + const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); + const activeFilterManager = filterManager; + + if (activeFilterManager != null) { + activeFilterManager.addFilters(filter); + if (onFilterAdded != null) { + onFilterAdded(); + } + } +}; + +export interface FilterForValueProps extends HoverActionComponentProps { + defaultFocusedButtonRef: EuiButtonIconPropsForButton['buttonRef']; +} + +export const FilterForValueButton: React.FC = React.memo( + ({ defaultFocusedButtonRef, field, onClick, ownFocus, showTooltip = false, value }) => { + return showTooltip ? ( + + } + > + + + ) : ( + + ); + } +); + +FilterForValueButton.displayName = 'FilterForValueButton'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx new file mode 100644 index 000000000000..a0888ac7c6e1 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx @@ -0,0 +1,77 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; +import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; +import { HoverActionComponentProps, FilterValueFnArgs } from './types'; + +export const FILTER_OUT_VALUE = i18n.translate('xpack.timelines.hoverActions.filterOutValue', { + defaultMessage: 'Filter out value', +}); + +export const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o'; + +export const filterOutValueFn = ({ + field, + value, + filterManager, + onFilterAdded, +}: FilterValueFnArgs) => { + const filter = + value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); + const activeFilterManager = filterManager; + + if (activeFilterManager != null) { + activeFilterManager.addFilters(filter); + if (onFilterAdded != null) { + onFilterAdded(); + } + } +}; + +export const FilterOutValueButton: React.FC = React.memo( + ({ field, onClick, ownFocus, showTooltip = false, value }) => { + return showTooltip ? ( + + } + > + + + ) : ( + + ); + } +); + +FilterOutValueButton.displayName = 'FilterOutValueButton'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts new file mode 100644 index 000000000000..4999638e0fe8 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FilterManager } from '../../../../../../../src/plugins/data/public'; + +export interface FilterValueFnArgs { + field: string; + value: string[] | string | null | undefined; + filterManager: FilterManager | undefined; + onFilterAdded: (() => void) | undefined; +} + +export interface HoverActionComponentProps { + field: string; + onClick?: () => void; + ownFocus: boolean; + showTooltip?: boolean; + value?: string[] | string | null; +} diff --git a/x-pack/plugins/timelines/public/components/hover_actions/index.tsx b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx new file mode 100644 index 000000000000..2329134d8562 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { + AddToTimelineButton, + ADD_TO_TIMELINE_KEYBOARD_SHORTCUT, + UseGetHandleStartDragToTimelineArgs, + useGetHandleStartDragToTimeline, +} from './actions/add_to_timeline'; +import { + ColumnToggleButton, + columnToggleFn, + ColumnToggleFnArgs, + ColumnToggleProps, + COLUMN_TOGGLE_KEYBOARD_SHORTCUT, +} from './actions/column_toggle'; +import { CopyButton, CopyProps, COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT } from './actions/copy'; +import { + FilterForValueButton, + filterForValueFn, + FilterForValueProps, + FILTER_FOR_VALUE_KEYBOARD_SHORTCUT, +} from './actions/filter_for_value'; +import { + FilterOutValueButton, + filterOutValueFn, + FILTER_OUT_VALUE_KEYBOARD_SHORTCUT, +} from './actions/filter_out_value'; +import { HoverActionComponentProps, FilterValueFnArgs } from './actions/types'; + +export interface HoverActionsConfig { + addToTimeline: { + AddToTimelineButton: React.FC; + keyboardShortcut: string; + useGetHandleStartDragToTimeline: (args: UseGetHandleStartDragToTimelineArgs) => () => void; + }; + columnToggle: { + ColumnToggleButton: React.FC; + columnToggleFn: (args: ColumnToggleFnArgs) => void; + keyboardShortcut: string; + }; + copy: { + CopyButton: React.FC; + keyboardShortcut: string; + }; + filterForValue: { + FilterForValueButton: React.FC; + filterForValueFn: (args: FilterValueFnArgs) => void; + keyboardShortcut: string; + }; + filterOutValue: { + FilterOutValueButton: React.FC; + filterOutValueFn: (args: FilterValueFnArgs) => void; + keyboardShortcut: string; + }; +} + +export const addToTimeline = { + AddToTimelineButton, + keyboardShortcut: ADD_TO_TIMELINE_KEYBOARD_SHORTCUT, + useGetHandleStartDragToTimeline, +}; + +export const columnToggle = { + ColumnToggleButton, + columnToggleFn, + keyboardShortcut: COLUMN_TOGGLE_KEYBOARD_SHORTCUT, +}; + +export const copy = { + CopyButton, + keyboardShortcut: COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT, +}; + +export const filterForValue = { + FilterForValueButton, + filterForValueFn, + keyboardShortcut: FILTER_FOR_VALUE_KEYBOARD_SHORTCUT, +}; + +export const filterOutValue = { + FilterOutValueButton, + filterOutValueFn, + keyboardShortcut: FILTER_OUT_VALUE_KEYBOARD_SHORTCUT, +}; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/utils.ts b/x-pack/plugins/timelines/public/components/hover_actions/utils.ts new file mode 100644 index 000000000000..f34506eaa795 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/utils.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Filter } from '../../../../../../src/plugins/data/public'; + +export const getAdditionalScreenReaderOnlyContext = ({ + field, + value, +}: { + field: string; + value?: string[] | string | null; +}): string => { + if (value == null) { + return field; + } + + return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`; +}; + +export const createFilter = ( + key: string, + value: string[] | string | null | undefined, + negate: boolean = false +): Filter => { + const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null; + return queryValue != null + ? { + meta: { + alias: null, + negate, + disabled: false, + type: 'phrase', + key, + value: queryValue, + params: { + query: queryValue, + }, + }, + query: { + match: { + [key]: { + query: queryValue, + type: 'phrase', + }, + }, + }, + } + : ({ + exists: { + field: key, + }, + meta: { + alias: null, + disabled: false, + key, + negate: value === undefined, + type: 'exists', + value: 'exists', + }, + } as Filter); +}; diff --git a/x-pack/plugins/timelines/public/components/tooltip_with_keyboard_shortcut/index.tsx b/x-pack/plugins/timelines/public/components/tooltip_with_keyboard_shortcut/index.tsx new file mode 100644 index 000000000000..fdf1f5275c17 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/tooltip_with_keyboard_shortcut/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiText, EuiScreenReaderOnly } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const PRESS = i18n.translate( + 'xpack.timelines.hoverActions.tooltipWithKeyboardShortcut.pressTooltipLabel', + { + defaultMessage: 'Press', + } +); +export interface TooltipWithKeyboardShortcutProps { + additionalScreenReaderOnlyContext?: string; + content: React.ReactNode; + shortcut: string; + showShortcut: boolean; +} + +const TooltipWithKeyboardShortcutComponent = ({ + additionalScreenReaderOnlyContext = '', + content, + shortcut, + showShortcut, +}: TooltipWithKeyboardShortcutProps) => ( + <> +
{content}
+ {additionalScreenReaderOnlyContext !== '' && ( + +

{additionalScreenReaderOnlyContext}

+
+ )} + {showShortcut && ( + + {PRESS} + {'\u00a0'} + {shortcut} + + )} + +); + +export const TooltipWithKeyboardShortcut = React.memo(TooltipWithKeyboardShortcutComponent); +TooltipWithKeyboardShortcut.displayName = 'TooltipWithKeyboardShortcut'; diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index c9534d4312e7..38fdc6149839 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -21,7 +21,7 @@ import type { LastUpdatedAtProps, LoadingPanelProps } from './components'; import { tGridReducer } from './store/t_grid/reducer'; import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline'; - +import * as hoverActions from './components/hover_actions'; export class TimelinesPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} private _store: Store | undefined; @@ -35,6 +35,9 @@ export class TimelinesPlugin implements Plugin { return {} as TimelinesUIStart; } return { + getHoverActions: () => { + return hoverActions; + }, getTGrid: (props: TGridProps) => { return getTGridLazy(props, { store: this._store, diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index ffef1ee35c83..b5a4b54cf5eb 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { ReactElement } from 'react'; import type { SensorAPI } from 'react-beautiful-dnd'; import { Store } from 'redux'; @@ -16,8 +17,10 @@ import type { import type { TGridIntegratedProps } from './components/t_grid/integrated'; import type { TGridStandaloneProps } from './components/t_grid/standalone'; import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline'; +import { HoverActionsConfig } from './components/hover_actions/index'; export * from './store/t_grid'; export interface TimelinesUIStart { + getHoverActions: () => HoverActionsConfig; getTGrid: ( props: GetTGridProps ) => ReactElement>; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1cdd712ea53b..13cb175ac908 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1951,26 +1951,14 @@ "home.sampleData.ecommerceSpec.totalRevenueTitle": "[e コマース] 合計収益", "home.sampleData.ecommerceSpecDescription": "e コマースの注文をトラッキングするサンプルデータ、ビジュアライゼーション、ダッシュボードです。", "home.sampleData.ecommerceSpecTitle": "サンプル e コマース注文", - "home.sampleData.flightsSpec.airlineCarrierTitle": "[フライト] 航空会社", "home.sampleData.flightsSpec.airportConnectionsTitle": "[フライト] 空港乗り継ぎ (空港にカーソルを合わせてください) ", - "home.sampleData.flightsSpec.averageTicketPriceTitle": "[フライト] 平均運賃", - "home.sampleData.flightsSpec.controlsTitle": "[フライト] コントロール", "home.sampleData.flightsSpec.delayBucketsTitle": "[フライト] 遅延バケット", "home.sampleData.flightsSpec.delaysAndCancellationsTitle": "[フライト] 遅延・欠航", - "home.sampleData.flightsSpec.delayTypeTitle": "[フライト] 遅延タイプ", "home.sampleData.flightsSpec.departuresCountMapTitle": "[フライト] 出発カウントマップ", "home.sampleData.flightsSpec.destinationWeatherTitle": "[フライト] 目的地の天候", - "home.sampleData.flightsSpec.flightCancellationsTitle": "[フライト] フライト欠航", - "home.sampleData.flightsSpec.flightCountAndAverageTicketPriceTitle": "[フライト] カウントと平均運賃", - "home.sampleData.flightsSpec.flightDelaysTitle": "[フライト] フライトの遅延", "home.sampleData.flightsSpec.flightLogTitle": "[フライト] 飛行記録", "home.sampleData.flightsSpec.globalFlightDashboardDescription": "ES-Air、Logstash Airways、Kibana Airlines、JetBeats のサンプル飛行データを分析します", "home.sampleData.flightsSpec.globalFlightDashboardTitle": "[フライト] グローバルフライトダッシュボード", - "home.sampleData.flightsSpec.markdownInstructionsTitle": "[フライト] マークダウンの指示", - "home.sampleData.flightsSpec.originCountryTitle": "[Flights] 出発国と到着国の比較", - "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[フライト] フライト欠航合計", - "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[フライト] フライト遅延合計", - "home.sampleData.flightsSpec.totalFlightsTitle": "[フライト] フライト合計", "home.sampleData.flightsSpecDescription": "飛行ルートを監視するサンプルデータ、ビジュアライゼーション、ダッシュボードです。", "home.sampleData.flightsSpecTitle": "サンプル飛行データ", "home.sampleData.logsSpec.fileTypeScatterPlotTitle": "[ログ] ファイルタイプ散布図", @@ -3572,7 +3560,9 @@ "telemetry.provideUsageStatisticsAriaName": "使用統計を提供", "telemetry.provideUsageStatisticsTitle": "使用統計を提供", "telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー", + "telemetry.securityData": "Endpoint Security データ", "telemetry.seeExampleOfClusterData": "収集する {clusterData} の例をご覧ください。", + "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "当社が収集する{clusterData}および{endpointSecurityData}の例をご覧ください。", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigAndLinkDescription": "使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", @@ -8051,7 +8041,6 @@ "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.featuredResults.title": "強調された結果", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.go.button": "Go", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.lastUpdated.heading": "最終更新", - "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.leaveUnassigned.field": "割り当てなし", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.preview.title": "プレビュー", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.reset.button": "リセット", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.resultDetail.label": "結果詳細", @@ -13286,7 +13275,6 @@ "xpack.maps.observability.uniqueCountMetricName": "ユニークカウント", "xpack.maps.sampleData.ecommerceSpec.mapsTitle": "[e コマース] 国別の注文", "xpack.maps.sampleData.flightaSpec.logsTitle": "[ログ ] 合計リクエスト数とバイト数", - "xpack.maps.sampleData.flightaSpec.mapsTitle": "[フライト] 出発地と目的地の飛行時間", "xpack.maps.sampleDataLinkLabel": "マップ", "xpack.maps.security.desc": "セキュリティレイヤー", "xpack.maps.security.disabledDesc": "セキュリティインデックスパターンが見つかりません。セキュリティを開始するには、[セキュリティ]>[概要]に移動します。", @@ -19841,13 +19829,9 @@ "xpack.securitySolution.documentationLinks.detectionsRequirements.text": "検出の前提条件と要件", "xpack.securitySolution.documentationLinks.mlJobCompatibility.text": "MLジョブの互換性", "xpack.securitySolution.documentationLinks.solutionRequirements.text": "Elasticセキュリティシステム要件", - "xpack.securitySolution.dragAndDrop.addToTimeline": "タイムライン調査に追加", "xpack.securitySolution.dragAndDrop.closeButtonLabel": "閉じる", "xpack.securitySolution.dragAndDrop.copyToClipboardTooltip": "クリップボードにコピー", "xpack.securitySolution.dragAndDrop.draggableKeyboardInstructionsNotDraggingScreenReaderOnly": "オプションは Enter キーを押します。ドラッグを開始するには、スペースを押します。", - "xpack.securitySolution.dragAndDrop.fieldLabel": "フィールド", - "xpack.securitySolution.dragAndDrop.filterForValueHoverAction": "値でフィルター", - "xpack.securitySolution.dragAndDrop.filterOutValueHoverAction": "値を除外", "xpack.securitySolution.dragAndDrop.youAreInADialogContainingOptionsScreenReaderOnly": "フィールド {fieldName} のオプションを含む、ダイアログを表示しています。Tab を押すと、オプションを操作します。Escape を押すと、終了します。", "xpack.securitySolution.draggables.field.categoryLabel": "カテゴリー", "xpack.securitySolution.draggables.field.fieldLabel": "フィールド", @@ -20138,7 +20122,6 @@ "xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field}フィールドはオブジェクトであり、列として追加できるネストされたフィールドに分解されます", "xpack.securitySolution.eventDetails.table": "表", "xpack.securitySolution.eventDetails.value": "値", - "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "{field} 列を表示", "xpack.securitySolution.eventRenderers.alertsDescription": "マルウェアまたはランサムウェアが防御、検出されたときにアラートが表示されます。", "xpack.securitySolution.eventRenderers.alertsName": "アラート", "xpack.securitySolution.eventRenderers.auditdDescriptionPart1": "監査イベントは、Linux Audit Framework からセキュリティ関連ログを通知します。", @@ -20673,7 +20656,6 @@ "xpack.securitySolution.overview.pageTitle": "セキュリティ", "xpack.securitySolution.overview.recentCasesSidebarTitle": "最近のケース", "xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近のタイムライン", - "xpack.securitySolution.overview.showTopTooltip": "上位の{fieldName}を表示", "xpack.securitySolution.overview.signalCountTitle": "検出アラート傾向", "xpack.securitySolution.overview.startedText": "セキュリティ情報およびイベント管理 (SIEM) へようこそ。はじめに{docs}や{data}をご参照ください。今後の機能に関する情報やチュートリアルは、{siemSolution}ページをご覧ください。", "xpack.securitySolution.overview.startedText.dataLinkText": "投入データ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 94af55cc0f3b..6e7ab7a33a0f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1963,26 +1963,14 @@ "home.sampleData.ecommerceSpec.totalRevenueTitle": "[电子商务] 总收入", "home.sampleData.ecommerceSpecDescription": "用于追踪电子商务订单的样例数据、可视化和仪表板。", "home.sampleData.ecommerceSpecTitle": "样例电子商务订单", - "home.sampleData.flightsSpec.airlineCarrierTitle": "[航班] 航空公司", "home.sampleData.flightsSpec.airportConnectionsTitle": "[航班] 机场航线 (将鼠标悬停在机场上) ", - "home.sampleData.flightsSpec.averageTicketPriceTitle": "[航班] 平均票价", - "home.sampleData.flightsSpec.controlsTitle": "[航班] 控制", "home.sampleData.flightsSpec.delayBucketsTitle": "[航班] 延误存储桶", "home.sampleData.flightsSpec.delaysAndCancellationsTitle": "[航班] 延误与取消", - "home.sampleData.flightsSpec.delayTypeTitle": "[航班] 延误类型", "home.sampleData.flightsSpec.departuresCountMapTitle": "[航班] 离港计数地图", "home.sampleData.flightsSpec.destinationWeatherTitle": "[航班] 到达地天气", - "home.sampleData.flightsSpec.flightCancellationsTitle": "[航班] 航班取消", - "home.sampleData.flightsSpec.flightCountAndAverageTicketPriceTitle": "[航班] 航班计数和平均票价", - "home.sampleData.flightsSpec.flightDelaysTitle": "[航班] 航班延误", "home.sampleData.flightsSpec.flightLogTitle": "[航班] 飞行日志", "home.sampleData.flightsSpec.globalFlightDashboardDescription": "分析 ES-Air、Logstash Airways、Kibana Airlines 和 JetBeats 的模拟航班数据", "home.sampleData.flightsSpec.globalFlightDashboardTitle": "[航班] 全球航班仪表板", - "home.sampleData.flightsSpec.markdownInstructionsTitle": "[航班] Markdown 说明", - "home.sampleData.flightsSpec.originCountryTitle": "[航班] 始发国/地区与到达国/地区", - "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[航班] 航班取消总数", - "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[航班] 航班延误总数", - "home.sampleData.flightsSpec.totalFlightsTitle": "[航班] 航班总数", "home.sampleData.flightsSpecDescription": "用于监测航班路线的样例数据、可视化和仪表板。", "home.sampleData.flightsSpecTitle": "样例航班数据", "home.sampleData.logsSpec.fileTypeScatterPlotTitle": "[日志] 文件类型散点图", @@ -3598,7 +3586,9 @@ "telemetry.provideUsageStatisticsAriaName": "提供使用情况统计", "telemetry.provideUsageStatisticsTitle": "提供使用情况统计", "telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明", + "telemetry.securityData": "终端安全数据", "telemetry.seeExampleOfClusterData": "查看我们收集的{clusterData}的示例。", + "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "查看我们收集的{clusterData}和 {endpointSecurityData}示例。", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigAndLinkDescription": "启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", @@ -8119,7 +8109,6 @@ "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.featuredResults.title": "精选结果", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.go.button": "执行", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.lastUpdated.heading": "上次更新时间", - "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.leaveUnassigned.field": "不分配", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.preview.title": "预览", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.reset.button": "重置", "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.resultDetail.label": "结果详情", @@ -13464,7 +13453,6 @@ "xpack.maps.observability.uniqueCountMetricName": "唯一计数", "xpack.maps.sampleData.ecommerceSpec.mapsTitle": "[电子商务] 订单 (按国家/地区) ", "xpack.maps.sampleData.flightaSpec.logsTitle": "[日志] 请求和字节总数", - "xpack.maps.sampleData.flightaSpec.mapsTitle": "[航班] 始发地和到达地航班时间", "xpack.maps.sampleDataLinkLabel": "地图", "xpack.maps.security.desc": "安全层", "xpack.maps.security.disabledDesc": "找不到安全索引模式。要开始使用“安全性”,请前往“安全性”>“概览”。", @@ -20139,13 +20127,9 @@ "xpack.securitySolution.documentationLinks.detectionsRequirements.text": "检测先决条件和要求", "xpack.securitySolution.documentationLinks.mlJobCompatibility.text": "ML 作业兼容性", "xpack.securitySolution.documentationLinks.solutionRequirements.text": "Elastic Security 系统要求", - "xpack.securitySolution.dragAndDrop.addToTimeline": "添加到时间线调查", "xpack.securitySolution.dragAndDrop.closeButtonLabel": "关闭", "xpack.securitySolution.dragAndDrop.copyToClipboardTooltip": "复制到剪贴板", "xpack.securitySolution.dragAndDrop.draggableKeyboardInstructionsNotDraggingScreenReaderOnly": "按 enter 键可显示选项,或按空格键开始拖动。", - "xpack.securitySolution.dragAndDrop.fieldLabel": "字段", - "xpack.securitySolution.dragAndDrop.filterForValueHoverAction": "筛留值", - "xpack.securitySolution.dragAndDrop.filterOutValueHoverAction": "筛除值", "xpack.securitySolution.dragAndDrop.youAreInADialogContainingOptionsScreenReaderOnly": "您在对话框中,其中包含 {fieldName} 字段的选项。按 tab 键导航选项。按 escape 退出。", "xpack.securitySolution.draggables.field.categoryLabel": "类别", "xpack.securitySolution.draggables.field.fieldLabel": "字段", @@ -20439,7 +20423,6 @@ "xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field} 字段是对象,并分解为可以添加为列的嵌套字段", "xpack.securitySolution.eventDetails.table": "表", "xpack.securitySolution.eventDetails.value": "值", - "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "查看 {field} 列", "xpack.securitySolution.eventRenderers.alertsDescription": "阻止或检测到恶意软件或勒索软件时,显示告警", "xpack.securitySolution.eventRenderers.alertsName": "告警", "xpack.securitySolution.eventRenderers.auditdDescriptionPart1": "审计事件传送 Linux 审计框架的安全相关日志。", @@ -21003,7 +20986,6 @@ "xpack.securitySolution.overview.pageTitle": "安全", "xpack.securitySolution.overview.recentCasesSidebarTitle": "最近案例", "xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近的时间线", - "xpack.securitySolution.overview.showTopTooltip": "显示排名靠前的{fieldName}", "xpack.securitySolution.overview.signalCountTitle": "检测告警趋势", "xpack.securitySolution.overview.startedText": "欢迎使用安全信息和事件管理 (SIEM)。首先阅读我们的{docs}或{data}。要了解即将推出的功能和教程,请访问我们的 {siemSolution}页面。", "xpack.securitySolution.overview.startedText.dataLinkText": "正在采集数据", diff --git a/x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts b/x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts new file mode 100644 index 000000000000..2a4e6ddcf58f --- /dev/null +++ b/x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const uptimeRuleFieldMap = { + // common fields + 'monitor.id': { + type: 'keyword', + }, + 'url.full': { + type: 'keyword', + }, + 'observer.geo.name': { + type: 'keyword', + }, + reason: { + type: 'text', + }, + // monitor status alert fields + 'error.message': { + type: 'text', + }, + 'agent.name': { + type: 'keyword', + }, + 'monitor.name': { + type: 'keyword', + }, + 'monitor.type': { + type: 'keyword', + }, + // tls alert fields + 'tls.server.x509.issuer.common_name': { + type: 'keyword', + }, + 'tls.server.x509.subject.common_name': { + type: 'keyword', + }, + 'tls.server.x509.not_after': { + type: 'date', + }, + 'tls.server.x509.not_before': { + type: 'date', + }, + 'tls.server.hash.sha256': { + type: 'keyword', + }, + // anomaly alert fields + 'anomaly.start': { + type: 'date', + }, + 'anomaly.bucket_span.minutes': { + type: 'keyword', + }, +} as const; + +export type UptimeRuleFieldMap = typeof uptimeRuleFieldMap; diff --git a/x-pack/plugins/uptime/common/translations.ts b/x-pack/plugins/uptime/common/translations.ts index 353c7fb5325f..5211438b8ec4 100644 --- a/x-pack/plugins/uptime/common/translations.ts +++ b/x-pack/plugins/uptime/common/translations.ts @@ -37,3 +37,78 @@ export const MonitorStatusTranslations = { defaultMessage: 'Alert when a monitor is down or an availability threshold is breached.', }), }; + +export const TlsTranslations = { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { + defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary} +`, + values: { + commonName: '{{state.commonName}}', + issuer: '{{state.issuer}}', + summary: '{{state.summary}}', + status: '{{state.status}}', + }, + }), + name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { + defaultMessage: 'Uptime TLS (Legacy)', + }), + description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { + defaultMessage: + 'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.', + }), +}; + +export const TlsTranslationsLegacy = { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { + defaultMessage: `Detected {count} TLS certificates expiring or becoming too old. +{expiringConditionalOpen} +Expiring cert count: {expiringCount} +Expiring Certificates: {expiringCommonNameAndDate} +{expiringConditionalClose} +{agingConditionalOpen} +Aging cert count: {agingCount} +Aging Certificates: {agingCommonNameAndDate} +{agingConditionalClose} +`, + values: { + count: '{{state.count}}', + expiringCount: '{{state.expiringCount}}', + expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}', + expiringConditionalOpen: '{{#state.hasExpired}}', + expiringConditionalClose: '{{/state.hasExpired}}', + agingCount: '{{state.agingCount}}', + agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}', + agingConditionalOpen: '{{#state.hasAging}}', + agingConditionalClose: '{{/state.hasAging}}', + }, + }), + name: i18n.translate('xpack.uptime.alerts.tls.clientName', { + defaultMessage: 'Uptime TLS', + }), + description: i18n.translate('xpack.uptime.alerts.tls.description', { + defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.', + }), +}; + +export const DurationAnomalyTranslations = { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', { + defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}. +Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`, + values: { + severity: '{{state.severity}}', + anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}', + monitor: '{{state.monitor}}', + monitorUrl: '{{{state.monitorUrl}}}', + slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}', + expectedResponseTime: '{{state.expectedResponseTime}}', + severityScore: '{{state.severityScore}}', + observerLocation: '{{state.observerLocation}}', + }, + }), + name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', { + defaultMessage: 'Uptime Duration Anomaly', + }), + description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', { + defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.', + }), +}; diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 97d154843231..e7fcb4607a8e 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,8 +1,16 @@ { - "configPath": ["xpack", "uptime"], + "configPath": [ + "xpack", + "uptime" + ], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": ["data", "home", "ml", "fleet"], + "optionalPlugins": [ + "data", + "home", + "ml", + "fleet" + ], "requiredPlugins": [ "alerting", "embeddable", @@ -10,15 +18,24 @@ "licensing", "triggersActionsUi", "usageCollection", + "ruleRegistry", "observability" ], "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "fleet"], + "requiredBundles": [ + "observability", + "kibanaReact", + "kibanaUtils", + "home", + "data", + "ml", + "fleet" + ], "owner": { "name": "Uptime", "githubTeam": "uptime" }, "description": "This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions." -} +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 869ecda3d29c..45067c6018cc 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { CoreSetup, CoreStart, @@ -29,7 +28,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; -import { alertTypeInitializers } from '../lib/alert_types'; +import { alertTypeInitializers, legacyAlertTypeInitializers } from '../lib/alert_types'; import { FleetStart } from '../../../fleet/public'; import { FetchDataParams, @@ -141,6 +140,36 @@ export class UptimePlugin ) ); + const { observabilityRuleTypeRegistry } = plugins.observability; + + core.getStartServices().then(([coreStart, clientPluginsStart]) => { + alertTypeInitializers.forEach((init) => { + const alertInitializer = init({ + core: coreStart, + plugins: clientPluginsStart, + }); + if ( + clientPluginsStart.triggersActionsUi && + !clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id) + ) { + observabilityRuleTypeRegistry.register(alertInitializer); + } + }); + + legacyAlertTypeInitializers.forEach((init) => { + const alertInitializer = init({ + core: coreStart, + plugins: clientPluginsStart, + }); + if ( + clientPluginsStart.triggersActionsUi && + !clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id) + ) { + plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer); + } + }); + }); + core.application.register({ id: PLUGIN.ID, euiIconType: 'logoObservability', @@ -171,26 +200,12 @@ export class UptimePlugin const [coreStart, corePlugins] = await core.getStartServices(); const { renderApp } = await import('./render_app'); - return renderApp(coreStart, plugins, corePlugins, params); }, }); } public start(start: CoreStart, plugins: ClientPluginsStart): void { - alertTypeInitializers.forEach((init) => { - const alertInitializer = init({ - core: start, - plugins, - }); - if ( - plugins.triggersActionsUi && - !plugins.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id) - ) { - plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer); - } - }); - if (plugins.fleet) { const { registerExtension } = plugins.fleet; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/common.ts b/x-pack/plugins/uptime/public/lib/alert_types/common.ts new file mode 100644 index 000000000000..09b02150957d --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/common.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { stringify } from 'querystring'; + +export const format = ({ + pathname, + query, +}: { + pathname: string; + query: Record; +}): string => { + return `${pathname}?${stringify(query)}`; +}; + +export const getMonitorRouteFromMonitorId = ({ + monitorId, + dateRangeStart, + dateRangeEnd, + filters = {}, +}: { + monitorId: string; + dateRangeStart: string; + dateRangeEnd: string; + filters?: Record; +}) => + format({ + pathname: `/app/uptime/monitor/${btoa(monitorId)}`, + query: { + dateRangeEnd, + dateRangeStart, + ...(Object.keys(filters).length + ? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) } + : {}), + }, + }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx index a80e38ac622a..f14c1a4a9fdd 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx @@ -6,18 +6,23 @@ */ import React from 'react'; -import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import moment from 'moment'; + import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; -import { DurationAnomalyTranslations } from './translations'; +import { DurationAnomalyTranslations } from '../../../common/translations'; import { AlertTypeInitializer } from '.'; +import { getMonitorRouteFromMonitorId } from './common'; + +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; + const { defaultActionMessage, description } = DurationAnomalyTranslations; const DurationAnomalyAlert = React.lazy(() => import('./lazy_wrapper/duration_anomaly')); export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ core, plugins, -}): AlertTypeModel => ({ +}): ObservabilityRuleTypeModel => ({ id: CLIENT_ALERT_TYPES.DURATION_ANOMALY, iconClass: 'uptimeApp', documentationUrl(docLinks) { @@ -30,4 +35,13 @@ export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ validate: () => ({ errors: {} }), defaultActionMessage, requiresAppContext: true, + format: ({ fields }) => ({ + reason: fields.reason, + link: getMonitorRouteFromMonitorId({ + monitorId: fields['monitor.id']!, + dateRangeEnd: + fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!, + dateRangeStart: moment(new Date(fields['anomaly.start']!)).subtract('5', 'm').toISOString(), + }), + }), }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts index 406b730fa1e6..9dc67340a043 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/index.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts @@ -6,6 +6,7 @@ */ import { CoreStart } from 'kibana/public'; +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { initMonitorStatusAlertType } from './monitor_status'; import { initTlsAlertType } from './tls'; @@ -13,14 +14,17 @@ import { initTlsLegacyAlertType } from './tls_legacy'; import { ClientPluginsStart } from '../../apps/plugin'; import { initDurationAnomalyAlertType } from './duration_anomaly'; -export type AlertTypeInitializer = (dependenies: { +export type AlertTypeInitializer = (dependenies: { core: CoreStart; plugins: ClientPluginsStart; -}) => AlertTypeModel; +}) => TAlertTypeModel; export const alertTypeInitializers: AlertTypeInitializer[] = [ initMonitorStatusAlertType, initTlsAlertType, - initTlsLegacyAlertType, initDurationAnomalyAlertType, ]; + +export const legacyAlertTypeInitializers: Array> = [ + initTlsLegacyAlertType, +]; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts index fc19d4c60e17..16c20cf7666e 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts @@ -206,6 +206,7 @@ describe('monitor status alert type', () => { "defaultActionMessage": "Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}", "description": "Alert when a monitor is down or an availability threshold is breached.", "documentationUrl": [Function], + "format": [Function], "iconClass": "uptimeApp", "id": "xpack.uptime.alerts.monitorStatus", "requiresAppContext": false, diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 50db7d9b5b5a..a87ba4aedbb2 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -6,12 +6,18 @@ */ import React from 'react'; -import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; -import { AlertTypeInitializer } from '.'; +import moment from 'moment'; + +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; +import { ValidationResult } from '../../../../triggers_actions_ui/public'; import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { MonitorStatusTranslations } from '../../../common/translations'; +import { getMonitorRouteFromMonitorId } from './common'; + +import { AlertTypeInitializer } from '.'; + const { defaultActionMessage, description } = MonitorStatusTranslations; const MonitorStatusAlert = React.lazy(() => import('./lazy_wrapper/monitor_status')); @@ -21,7 +27,7 @@ let validateFunc: (alertParams: any) => ValidationResult; export const initMonitorStatusAlertType: AlertTypeInitializer = ({ core, plugins, -}): AlertTypeModel => ({ +}): ObservabilityRuleTypeModel => ({ id: CLIENT_ALERT_TYPES.MONITOR_STATUS, description, iconClass: 'uptimeApp', @@ -44,4 +50,18 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({ }, defaultActionMessage, requiresAppContext: false, + format: ({ fields }) => ({ + reason: fields.reason, + link: getMonitorRouteFromMonitorId({ + monitorId: fields['monitor.id']!, + dateRangeEnd: + fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!, + dateRangeStart: moment(new Date(fields['kibana.rac.alert.start']!)) + .subtract('5', 'm') + .toISOString(), + filters: { + 'observer.geo.name': [fields['observer.geo.name'][0]], + }, + }), + }), }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx index c3bcfc46646d..6632a0c04396 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx @@ -6,14 +6,19 @@ */ import React from 'react'; -import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; -import { TlsTranslations } from './translations'; +import { TlsTranslations } from '../../../common/translations'; import { AlertTypeInitializer } from '.'; +import { CERTIFICATES_ROUTE } from '../../../common/constants/ui'; + const { defaultActionMessage, description } = TlsTranslations; const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); -export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({ +export const initTlsAlertType: AlertTypeInitializer = ({ + core, + plugins, +}): ObservabilityRuleTypeModel => ({ id: CLIENT_ALERT_TYPES.TLS, iconClass: 'uptimeApp', documentationUrl(docLinks) { @@ -26,4 +31,8 @@ export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): Alert validate: () => ({ errors: {} }), defaultActionMessage, requiresAppContext: false, + format: ({ fields }) => ({ + reason: fields.reason, + link: `/app/uptime${CERTIFICATES_ROUTE}`, + }), }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx index 9982eb385d90..bed5dae55571 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; -import { TlsTranslationsLegacy } from './translations'; +import { TlsTranslationsLegacy } from '../../../common/translations'; import { AlertTypeInitializer } from '.'; const { defaultActionMessage, description } = TlsTranslationsLegacy; const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); -export const initTlsLegacyAlertType: AlertTypeInitializer = ({ +export const initTlsLegacyAlertType: AlertTypeInitializer = ({ core, plugins, }): AlertTypeModel => ({ diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts deleted file mode 100644 index 5122120479cf..000000000000 --- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts +++ /dev/null @@ -1,83 +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 { i18n } from '@kbn/i18n'; - -export const TlsTranslations = { - defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { - defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary} -`, - values: { - commonName: '{{state.commonName}}', - issuer: '{{state.issuer}}', - summary: '{{state.summary}}', - status: '{{state.status}}', - }, - }), - name: i18n.translate('xpack.uptime.alerts.tls.clientName', { - defaultMessage: 'Uptime TLS', - }), - description: i18n.translate('xpack.uptime.alerts.tls.description', { - defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.', - }), -}; - -export const TlsTranslationsLegacy = { - defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { - defaultMessage: `Detected {count} TLS certificates expiring or becoming too old. -{expiringConditionalOpen} -Expiring cert count: {expiringCount} -Expiring Certificates: {expiringCommonNameAndDate} -{expiringConditionalClose} -{agingConditionalOpen} -Aging cert count: {agingCount} -Aging Certificates: {agingCommonNameAndDate} -{agingConditionalClose} -`, - values: { - count: '{{state.count}}', - expiringCount: '{{state.expiringCount}}', - expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}', - expiringConditionalOpen: '{{#state.hasExpired}}', - expiringConditionalClose: '{{/state.hasExpired}}', - agingCount: '{{state.agingCount}}', - agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}', - agingConditionalOpen: '{{#state.hasAging}}', - agingConditionalClose: '{{/state.hasAging}}', - }, - }), - name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { - defaultMessage: 'Uptime TLS', - }), - description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { - defaultMessage: - 'Alert when the TLS certificate of an Uptime monitor is about to expire. This rule type will be deprecated in a future version.', - }), -}; - -export const DurationAnomalyTranslations = { - defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', { - defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}. -Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`, - values: { - severity: '{{state.severity}}', - anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}', - monitor: '{{state.monitor}}', - monitorUrl: '{{{state.monitorUrl}}}', - slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}', - expectedResponseTime: '{{state.expectedResponseTime}}', - severityScore: '{{state.severityScore}}', - observerLocation: '{{state.observerLocation}}', - }, - }), - name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', { - defaultMessage: 'Uptime Duration Anomaly', - }), - description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', { - defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.', - }), -}; diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 82ba70155608..c303c7827333 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -6,12 +6,14 @@ */ import { Request, Server } from '@hapi/hapi'; +import { Logger } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PLUGIN } from '../common/constants/plugin'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework'; import { umDynamicSettings } from './lib/saved_objects'; +import { UptimeRuleRegistry } from './plugin'; export interface KibanaRouteOptions { path: string; @@ -25,7 +27,12 @@ export interface KibanaServer extends Server { route: (options: KibanaRouteOptions) => void; } -export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => { +export const initServerWithKibana = ( + server: UptimeCoreSetup, + plugins: UptimeCorePlugins, + ruleRegistry: UptimeRuleRegistry, + logger: Logger +) => { const { features } = plugins; const libs = compose(server); @@ -86,5 +93,5 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor }, }); - initUptimeServer(server, libs, plugins); + initUptimeServer(server, libs, plugins, ruleRegistry, logger); }; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index d1bbbc1d1856..d5b938d78c86 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -11,9 +11,11 @@ import type { ISavedObjectsRepository, IScopedClusterClient, } from 'src/core/server'; +import { ObservabilityPluginSetup } from '../../../../../observability/server'; import { UMKibanaRoute } from '../../../rest_api'; import { PluginSetupContract } from '../../../../../features/server'; import { MlPluginSetup as MlSetup } from '../../../../../ml/server'; +import { RuleRegistryPluginSetupContract } from '../../../../../rule_registry/server'; import { UptimeESClient } from '../../lib'; import type { UptimeRouter } from '../../../types'; @@ -37,8 +39,10 @@ export interface UptimeCorePlugins { features: PluginSetupContract; alerting: any; elasticsearch: any; + observability: ObservabilityPluginSetup; usageCollection: UsageCollectionSetup; ml: MlSetup; + ruleRegistry: RuleRegistryPluginSetupContract; } export interface UMBackendFrameworkAdapter { diff --git a/x-pack/plugins/uptime/server/lib/alerts/common.ts b/x-pack/plugins/uptime/server/lib/alerts/common.ts index 29f2c0bde208..6bf9d28c2da9 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/common.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/common.ts @@ -6,6 +6,7 @@ */ import { isRight } from 'fp-ts/lib/Either'; +import Mustache from 'mustache'; import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types'; export type UpdateUptimeAlertState = ( @@ -55,3 +56,7 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => { isTriggered: isTriggeredNow, }; }; + +export const generateAlertMessage = (messageTemplate: string, fields: Record) => { + return Mustache.render(messageTemplate, { state: { ...fields } }); +}; diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts new file mode 100644 index 000000000000..ce13ae4ce6ce --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts @@ -0,0 +1,210 @@ +/* + * 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 { durationAnomalyAlertFactory } from './duration_anomaly'; +import { DURATION_ANOMALY } from '../../../common/constants/alerts'; +import { AnomaliesTableRecord, AnomalyRecordDoc } from '../../../../ml/common/types/anomalies'; +import { DynamicSettings } from '../../../common/runtime_types'; +import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; +import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; +import { Ping } from '../../../common/runtime_types/ping'; +import { + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_EVALUATION_VALUE, + ALERT_EVALUATION_THRESHOLD, +} from '@kbn/rule-data-utils/target/technical_field_names'; + +interface MockAnomaly { + severity: AnomaliesTableRecord['severity']; + source: Partial; + actualSort: AnomaliesTableRecord['actualSort']; + typicalSort: AnomaliesTableRecord['typicalSort']; + entityValue: AnomaliesTableRecord['entityValue']; +} + +interface MockAnomalyResult { + anomalies: MockAnomaly[]; +} + +const monitorId = 'uptime-monitor'; +const mockUrl = 'https://elastic.co'; + +/** + * This function aims to provide an easy way to give mock props that will + * reduce boilerplate for tests. + * @param dynamic the expiration and aging thresholds received at alert creation time + * @param params the params received at alert creation time + * @param state the state the alert maintains + */ +const mockOptions = ( + dynamicCertSettings?: { + certExpirationThreshold: DynamicSettings['certExpirationThreshold']; + certAgeThreshold: DynamicSettings['certAgeThreshold']; + }, + state = {}, + params = { + timerange: { from: 'now-15m', to: 'now' }, + monitorId, + severity: 'warning', + } +): any => { + const { services } = createRuleTypeMocks(dynamicCertSettings); + + return { + params, + state, + services, + }; +}; + +const mockAnomaliesResult: MockAnomalyResult = { + anomalies: [ + { + severity: 25, + source: { + timestamp: 1622137799, + 'monitor.id': 'uptime-monitor', + bucket_span: 900, + }, + actualSort: 200000, + typicalSort: 10000, + entityValue: 'harrisburg', + }, + { + severity: 10, + source: { + timestamp: 1622137799, + 'monitor.id': 'uptime-monitor', + bucket_span: 900, + }, + actualSort: 300000, + typicalSort: 20000, + entityValue: 'fairbanks', + }, + ], +}; + +const mockPing: Partial = { + url: { + full: mockUrl, + }, +}; + +describe('duration anomaly alert', () => { + let toISOStringSpy: jest.SpyInstance; + const mockDate = 'date'; + beforeAll(() => { + Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z')); + jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({ + format: jest.fn(), + formatToParts: jest.fn(), + resolvedOptions: () => ({ + locale: '', + calendar: '', + numberingSystem: '', + timeZone: 'UTC', + }), + })); + toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString'); + }); + + describe('alert executor', () => { + it('triggers when aging or expiring alerts are found', async () => { + toISOStringSpy.mockImplementation(() => mockDate); + const mockResultServiceProviderGetter: jest.Mock<{ + getAnomaliesTableData: jest.Mock; + }> = jest.fn(); + const mockGetAnomliesTableDataGetter: jest.Mock = jest.fn(); + const mockGetLatestMonitorGetter: jest.Mock> = jest.fn(); + + mockGetLatestMonitorGetter.mockReturnValue(mockPing); + mockGetAnomliesTableDataGetter.mockReturnValue(mockAnomaliesResult); + mockResultServiceProviderGetter.mockReturnValue({ + getAnomaliesTableData: mockGetAnomliesTableDataGetter, + }); + const { server, libs, plugins } = bootstrapDependencies( + { getLatestMonitor: mockGetLatestMonitorGetter }, + { + ml: { + resultsServiceProvider: mockResultServiceProviderGetter, + }, + } + ); + const alert = durationAnomalyAlertFactory(server, libs, plugins); + const options = mockOptions(); + const { + services: { alertWithLifecycle }, + } = options; + // @ts-ignore the executor can return `void`, but ours never does + const state: Record = await alert.executor(options); + expect(mockGetAnomliesTableDataGetter).toHaveBeenCalledTimes(1); + expect(alertWithLifecycle).toHaveBeenCalledTimes(2); + expect(mockGetAnomliesTableDataGetter).toBeCalledWith( + ['uptime_monitor_high_latency_by_geo'], + [], + [], + 'auto', + options.params.severity, + 1620909217000, + 1620909217000, + 'UTC', + 500, + 10, + undefined + ); + const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); + mockAnomaliesResult.anomalies.forEach((anomaly, index) => { + const slowestResponse = Math.round(anomaly.actualSort / 1000); + const typicalResponse = Math.round(anomaly.typicalSort / 1000); + expect(alertWithLifecycle).toBeCalledWith({ + fields: { + 'monitor.id': options.params.monitorId, + 'url.full': mockPing.url?.full, + 'anomaly.start': mockDate, + 'anomaly.bucket_span.minutes': anomaly.source.bucket_span, + 'observer.geo.name': anomaly.entityValue, + [ALERT_EVALUATION_VALUE]: anomaly.actualSort, + [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort, + [ALERT_SEVERITY_LEVEL]: getSeverityType(anomaly.severity), + [ALERT_SEVERITY_VALUE]: anomaly.severity, + reason: `Abnormal (${getSeverityType( + anomaly.severity + )} level) response time detected on uptime-monitor with url ${ + mockPing.url?.full + } at date. Anomaly severity score is ${anomaly.severity}. +Response times as high as ${slowestResponse} ms have been detected from location ${ + anomaly.entityValue + }. Expected response time is ${typicalResponse} ms.`, + }, + id: `${DURATION_ANOMALY.id}${index}`, + }); + expect(alertInstanceMock.replaceState).toBeCalledWith({ + firstCheckedAt: 'date', + firstTriggeredAt: undefined, + lastCheckedAt: 'date', + lastResolvedAt: undefined, + isTriggered: false, + anomalyStartTimestamp: 'date', + currentTriggerStarted: undefined, + expectedResponseTime: `${typicalResponse} ms`, + lastTriggeredAt: undefined, + monitor: monitorId, + monitorUrl: mockPing.url?.full, + observerLocation: anomaly.entityValue, + severity: getSeverityType(anomaly.severity), + severityScore: anomaly.severity, + slowestAnomalyResponse: `${slowestResponse} ms`, + bucketSpan: anomaly.source.bucket_span, + }); + }); + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2); + expect(alertInstanceMock.scheduleActions).toBeCalledWith(DURATION_ANOMALY.id); + }); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 981a7e7ca392..2388a789f3b8 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -8,8 +8,14 @@ import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import moment from 'moment'; import { schema } from '@kbn/config-schema'; +import { + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_EVALUATION_VALUE, + ALERT_EVALUATION_THRESHOLD, +} from '@kbn/rule-data-utils/target/technical_field_names'; import { ActionGroupIdsOf } from '../../../../alerting/common'; -import { updateState } from './common'; +import { updateState, generateAlertMessage } from './common'; import { DURATION_ANOMALY } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; @@ -18,8 +24,10 @@ import { UptimeCorePlugins } from '../adapters/framework'; import { UptimeAlertTypeFactory } from './types'; import { Ping } from '../../../common/runtime_types/ping'; import { getMLJobId } from '../../../common/lib'; -import { getLatestMonitor } from '../requests/get_latest_monitor'; -import { uptimeAlertWrapper } from './uptime_alert_wrapper'; + +import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations'; + +import { createUptimeESClient } from '../lib'; export type ActionGroupIds = ActionGroupIdsOf; @@ -33,6 +41,7 @@ export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Pi slowestAnomalyResponse: Math.round(anomaly.actualSort / 1000) + ' ms', expectedResponseTime: Math.round(anomaly.typicalSort / 1000) + ' ms', observerLocation: anomaly.entityValue, + bucketSpan: anomaly.source.bucket_span, }; }; @@ -65,61 +74,83 @@ const getAnomalies = async ( export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = ( _server, - _libs, + libs, plugins -) => - uptimeAlertWrapper({ - id: 'xpack.uptime.alerts.durationAnomaly', - name: durationAnomalyTranslations.alertFactoryName, - validate: { - params: schema.object({ - monitorId: schema.string(), - severity: schema.number(), - }), - }, - defaultActionGroupId: DURATION_ANOMALY.id, - actionGroups: [ - { - id: DURATION_ANOMALY.id, - name: DURATION_ANOMALY.name, - }, - ], - actionVariables: { - context: [], - state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], +) => ({ + id: 'xpack.uptime.alerts.durationAnomaly', + producer: 'uptime', + name: durationAnomalyTranslations.alertFactoryName, + validate: { + params: schema.object({ + monitorId: schema.string(), + severity: schema.number(), + }), + }, + defaultActionGroupId: DURATION_ANOMALY.id, + actionGroups: [ + { + id: DURATION_ANOMALY.id, + name: DURATION_ANOMALY.name, }, - minimumLicenseRequired: 'basic', - isExportable: true, - async executor({ options, uptimeEsClient, savedObjectsClient, dynamicSettings }) { - const { - services: { alertInstanceFactory }, - state, - params, - } = options; + ], + actionVariables: { + context: [], + state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], + }, + isExportable: true, + minimumLicenseRequired: 'platinum', + async executor({ + params, + services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient }, + state, + }) { + const uptimeEsClient = createUptimeESClient({ + esClient: scopedClusterClient.asCurrentUser, + savedObjectsClient, + }); + const { anomalies } = + (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt as string)) ?? + {}; - const { anomalies } = - (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt)) ?? {}; + const foundAnomalies = anomalies?.length > 0; - const foundAnomalies = anomalies?.length > 0; + if (foundAnomalies) { + const monitorInfo = await libs.requests.getLatestMonitor({ + uptimeEsClient, + dateStart: 'now-15m', + dateEnd: 'now', + monitorId: params.monitorId, + }); - if (foundAnomalies) { - const monitorInfo = await getLatestMonitor({ - uptimeEsClient, - dateStart: 'now-15m', - dateEnd: 'now', - monitorId: params.monitorId, + anomalies.forEach((anomaly, index) => { + const summary = getAnomalySummary(anomaly, monitorInfo); + + const alertInstance = alertWithLifecycle({ + id: DURATION_ANOMALY.id + index, + fields: { + 'monitor.id': params.monitorId, + 'url.full': summary.monitorUrl, + 'observer.geo.name': summary.observerLocation, + 'anomaly.start': summary.anomalyStartTimestamp, + 'anomaly.bucket_span.minutes': summary.bucketSpan, + [ALERT_EVALUATION_VALUE]: anomaly.actualSort, + [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort, + [ALERT_SEVERITY_LEVEL]: summary.severity, + [ALERT_SEVERITY_VALUE]: summary.severityScore, + reason: generateAlertMessage( + CommonDurationAnomalyTranslations.defaultActionMessage, + summary + ), + }, }); - anomalies.forEach((anomaly, index) => { - const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index); - const summary = getAnomalySummary(anomaly, monitorInfo); - alertInstance.replaceState({ - ...updateState(state, false), - ...summary, - }); - alertInstance.scheduleActions(DURATION_ANOMALY.id); + alertInstance.replaceState({ + ...updateState(state, false), + ...summary, }); - } + alertInstance.scheduleActions(DURATION_ANOMALY.id); + }); + } - return updateState(state, foundAnomalies); - }, - }); + return updateState(state, foundAnomalies); + }, +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index 743e9f6bc75a..dbb199a2e07d 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -10,47 +10,99 @@ import { statusCheckAlertFactory, getStatusMessage, getUniqueIdsByLoc, + getInstanceId, } from './status_check'; -import { - AlertType, - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, -} from '../../../../alerting/server'; -import { UMServerLibs } from '../lib'; -import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; -import { alertsMock, AlertServicesMock } from '../../../../alerting/server/mocks'; import { GetMonitorStatusResult } from '../requests/get_monitor_status'; import { makePing } from '../../../common/runtime_types/ping'; import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability'; -import type { UptimeRouter } from '../../types'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { DefaultUptimeAlertInstance } from './types'; +import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; -/** - * The alert takes some dependencies as parameters; these are things like - * kibana core services and plugins. This function helps reduce the amount of - * boilerplate required. - * @param customRequests client tests can use this paramter to provide their own request mocks, - * so we don't have to mock them all for each test. - */ -const bootstrapDependencies = (customRequests?: any) => { - const router = {} as UptimeRouter; - // these server/libs parameters don't have any functionality, which is fine - // because we aren't testing them here - const server: UptimeCoreSetup = { router }; - const plugins: UptimeCorePlugins = {} as any; - const libs: UMServerLibs = { requests: {} } as UMServerLibs; - libs.requests = { ...libs.requests, ...customRequests }; - return { server, libs, plugins }; +const mockMonitors = [ + { + monitorId: 'first', + location: 'harrisburg', + count: 234, + status: 'down', + monitorInfo: { + ...makePing({ + id: 'first', + location: 'harrisburg', + url: 'localhost:8080', + }), + error: { + message: 'error message 1', + }, + }, + }, + { + monitorId: 'first', + location: 'fairbanks', + count: 234, + status: 'down', + monitorInfo: { + ...makePing({ + id: 'first', + location: 'fairbanks', + url: 'localhost:5601', + }), + error: { + message: 'error message 2', + }, + }, + }, +]; + +const mockCommonAlertDocumentFields = (monitorInfo: GetMonitorStatusResult['monitorInfo']) => ({ + 'agent.name': monitorInfo.agent?.name, + 'error.message': monitorInfo.error?.message, + 'monitor.id': monitorInfo.monitor.id, + 'monitor.name': monitorInfo.monitor.name || monitorInfo.monitor.id, + 'monitor.type': monitorInfo.monitor.type, + 'url.full': monitorInfo.url?.full, + 'observer.geo.name': monitorInfo.observer?.geo?.name, +}); + +const mockStatusAlertDocument = ( + monitor: GetMonitorStatusResult, + isAutoGenerated: boolean = false +) => { + const { monitorInfo } = monitor; + return { + fields: { + ...mockCommonAlertDocumentFields(monitor.monitorInfo), + reason: `Monitor first with url ${monitorInfo?.url?.full} is down from ${ + monitorInfo.observer?.geo?.name + }. The latest error message is ${monitorInfo.error?.message || ''}`, + }, + id: getInstanceId( + monitorInfo, + `${isAutoGenerated ? '' : monitorInfo?.monitor.id + '-'}${monitorInfo.observer?.geo?.name}` + ), + }; +}; + +const mockAvailabilityAlertDocument = (monitor: GetMonitorAvailabilityResult) => { + const { monitorInfo } = monitor; + return { + fields: { + ...mockCommonAlertDocumentFields(monitor.monitorInfo), + reason: `Monitor ${monitorInfo.monitor.name || monitorInfo.monitor.id} with url ${ + monitorInfo?.url?.full + } is below threshold with ${(monitor.availabilityRatio! * 100).toFixed( + 2 + )}% availability expected is 99.34% from ${ + monitorInfo.observer?.geo?.name + }. The latest error message is ${monitorInfo.error?.message || ''}`, + }, + id: getInstanceId(monitorInfo, `${monitorInfo?.monitor.id}-${monitorInfo.observer?.geo?.name}`), + }; }; /** * This function aims to provide an easy way to give mock props that will * reduce boilerplate for tests. * @param params the params received at alert creation time - * @param services the core services provided by kibana/alerting platforms * @param state the state the alert maintains */ const mockOptions = ( @@ -60,7 +112,6 @@ const mockOptions = ( timerange: { from: 'now-15m', to: 'now' }, shouldCheckStatus: true, }, - services = alertsMock.createAlertServices(), state = {}, rule = { schedule: { @@ -68,19 +119,12 @@ const mockOptions = ( }, } ): any => { - services.scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - services.scopedClusterClient.asCurrentUser = (jest.fn() as unknown) as any; + const { services } = createRuleTypeMocks(); - services.savedObjectsClient.get.mockResolvedValue({ - id: '', - type: '', - references: [], - attributes: DYNAMIC_SETTINGS_DEFAULTS, - }); return { params, - services, state, + services, rule, }; }; @@ -98,100 +142,134 @@ describe('status check alert', () => { }); describe('executor', () => { it('does not trigger when there are no monitors down', async () => { - expect.assertions(4); + expect.assertions(5); const mockGetter = jest.fn(); mockGetter.mockReturnValue([]); const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); const alert = statusCheckAlertFactory(server, libs, plugins); // @ts-ignore the executor can return `void`, but ours never does - const state: Record = await alert.executor(mockOptions()); + const options = mockOptions(); + const state: Record = await alert.executor(options); + const { + services: { alertWithLifecycle }, + } = options; expect(state).not.toBeUndefined(); expect(state?.isTriggered).toBe(false); + expect(alertWithLifecycle).not.toHaveBeenCalled(); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": undefined, - "locations": Array [], - "numTimes": 5, - "timespanRange": Object { - "from": "now-15m", - "to": "now", - }, - "timestampRange": Object { - "from": 1620821917000, - "to": "now", - }, - "uptimeEsClient": Object { - "baseESClient": [MockFunction], - "count": [Function], - "getSavedObjectsClient": [Function], - "search": [Function], - }, + expect(mockGetter.mock.calls[0][0]).toEqual( + expect.objectContaining({ + filters: undefined, + locations: [], + numTimes: 5, + timespanRange: { + from: 'now-15m', + to: 'now', }, - ] - `); + timestampRange: { + from: 1620821917000, + to: 'now', + }, + }) + ); }); it('triggers when monitors are down and provides expected state', async () => { toISOStringSpy.mockImplementation(() => 'foo date string'); const mockGetter: jest.Mock = jest.fn(); - mockGetter.mockReturnValue([ - { - monitorId: 'first', - location: 'harrisburg', - count: 234, - status: 'down', - monitorInfo: makePing({ - id: 'first', - location: 'harrisburg', - }), - }, - { - monitorId: 'first', - location: 'fairbanks', - count: 234, - status: 'down', - monitorInfo: makePing({ - id: 'first', - location: 'fairbanks', - }), - }, - ]); + mockGetter.mockReturnValue(mockMonitors); const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions(); - const alertServices: AlertServicesMock = options.services; + const { + services: { alertWithLifecycle }, + } = options; // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledTimes(2); - expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` + expect(alertWithLifecycle).toHaveBeenCalledTimes(2); + mockMonitors.forEach((monitor) => { + expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor)); + }); + expect(mockGetter.mock.calls[0][0]).toEqual( + expect.objectContaining({ + filters: undefined, + locations: [], + numTimes: 5, + timespanRange: { + from: 'now-15m', + to: 'now', + }, + }) + ); + const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); + expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "filters": undefined, - "locations": Array [], - "numTimes": 5, - "timespanRange": Object { - "from": "now-15m", - "to": "now", - }, - "timestampRange": Object { - "from": 1620821917000, - "to": "now", - }, - "uptimeEsClient": Object { - "baseESClient": [MockFunction], - "count": [Function], - "getSavedObjectsClient": [Function], - "search": [Function], - }, + "currentTriggerStarted": "foo date string", + "firstCheckedAt": "foo date string", + "firstTriggeredAt": "foo date string", + "isTriggered": true, + "lastCheckedAt": "foo date string", + "lastResolvedAt": undefined, + "lastTriggeredAt": "foo date string", + "latestErrorMessage": "error message 1", + "monitorId": "first", + "monitorName": "first", + "monitorType": "myType", + "monitorUrl": "localhost:8080", + "observerHostname": undefined, + "observerLocation": "harrisburg", + "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1", + "statusMessage": "down", }, ] `); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2); + expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "xpack.uptime.alerts.actionGroups.monitorStatus", + ] + `); + }); + + it('supports auto generated monitor status alerts', async () => { + toISOStringSpy.mockImplementation(() => 'foo date string'); + const mockGetter: jest.Mock = jest.fn(); + + mockGetter.mockReturnValue(mockMonitors); + const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); + const alert = statusCheckAlertFactory(server, libs, plugins); + const options = mockOptions({ + isAutoGenerated: true, + timerange: { from: 'now-15m', to: 'now' }, + numTimes: 5, + }); + const { + services: { alertWithLifecycle }, + } = options; + // @ts-ignore the executor can return `void`, but ours never does + const state: Record = await alert.executor(options); + expect(mockGetter).toHaveBeenCalledTimes(1); + expect(alertWithLifecycle).toHaveBeenCalledTimes(2); + mockMonitors.forEach((monitor) => { + expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor, true)); + }); + expect(mockGetter.mock.calls[0][0]).toEqual( + expect.objectContaining({ + filters: undefined, + locations: [], + numTimes: 5, + timespanRange: { + from: 'now-15m', + to: 'now', + }, + }) + ); + const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -203,13 +281,14 @@ describe('status check alert', () => { "lastCheckedAt": "foo date string", "lastResolvedAt": undefined, "lastTriggeredAt": "foo date string", - "latestErrorMessage": undefined, + "latestErrorMessage": "error message 1", "monitorId": "first", "monitorName": "first", "monitorType": "myType", - "monitorUrl": undefined, + "monitorUrl": "localhost:8080", "observerHostname": undefined, "observerLocation": "harrisburg", + "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1", "statusMessage": "down", }, ] @@ -218,9 +297,6 @@ describe('status check alert', () => { expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "message": "Monitor first with url is down from harrisburg. The latest error message is ", - }, ] `); }); @@ -229,29 +305,7 @@ describe('status check alert', () => { toISOStringSpy.mockImplementation(() => '7.7 date'); const mockGetter: jest.Mock = jest.fn(); - mockGetter.mockReturnValue([ - { - monitorId: 'first', - location: 'harrisburg', - count: 234, - status: 'down', - monitorInfo: makePing({ - id: 'first', - location: 'harrisburg', - }), - }, - { - monitorId: 'first', - location: 'fairbanks', - count: 234, - status: 'down', - - monitorInfo: makePing({ - id: 'first', - location: 'fairbanks', - }), - }, - ]); + mockGetter.mockReturnValue(mockMonitors); const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter, getIndexPattern: jest.fn(), @@ -259,14 +313,19 @@ describe('status check alert', () => { const alert = statusCheckAlertFactory(server, libs, plugins); const options = mockOptions({ numTimes: 4, - timerange: { from: 'now-14h', to: 'now' }, + timespanRange: { from: 'now-14h', to: 'now' }, locations: ['fairbanks'], filters: '', }); - const alertServices: AlertServicesMock = options.services; + const { + services: { alertWithLifecycle }, + } = options; const state = await alert.executor(options); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + mockMonitors.forEach((monitor) => { + expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor)); + }); expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -278,13 +337,14 @@ describe('status check alert', () => { "lastCheckedAt": "7.7 date", "lastResolvedAt": undefined, "lastTriggeredAt": "7.7 date", - "latestErrorMessage": undefined, + "latestErrorMessage": "error message 1", "monitorId": "first", "monitorName": "first", "monitorType": "myType", - "monitorUrl": undefined, + "monitorUrl": "localhost:8080", "observerHostname": undefined, "observerLocation": "harrisburg", + "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1", "statusMessage": "down", }, ] @@ -303,33 +363,11 @@ describe('status check alert', () => { }); it('supports 7.8 alert format', async () => { - expect.assertions(5); + expect.assertions(8); toISOStringSpy.mockImplementation(() => 'foo date string'); const mockGetter: jest.Mock = jest.fn(); - mockGetter.mockReturnValueOnce([ - { - monitorId: 'first', - location: 'harrisburg', - count: 234, - status: 'down', - monitorInfo: makePing({ - id: 'first', - location: 'harrisburg', - }), - }, - { - monitorId: 'first', - location: 'fairbanks', - count: 234, - status: 'down', - - monitorInfo: makePing({ - id: 'first', - location: 'fairbanks', - }), - }, - ]); + mockGetter.mockReturnValueOnce(mockMonitors); const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter, getIndexPattern: jest.fn(), @@ -347,166 +385,160 @@ describe('status check alert', () => { tags: ['unsecured', 'containers', 'org:google'], }, }); - const alertServices: AlertServicesMock = options.services; const state = await alert.executor(options); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const { + services: { alertWithLifecycle }, + } = options; + const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + mockMonitors.forEach((monitor) => { + expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor)); + }); expect(mockGetter).toHaveBeenCalledTimes(1); - - expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": Object { - "bool": Object { - "filter": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "url.port": "12349", - }, - }, - ], - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "url.port": "5601", - }, - }, - ], - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "url.port": "443", - }, - }, - ], + expect(mockGetter.mock.calls[0][0]).toEqual( + expect.objectContaining({ + locations: [], + numTimes: 3, + timespanRange: { + from: 'now-15m', + to: 'now', + }, + }) + ); + expect(mockGetter.mock.calls[0][0].filters).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "url.port": "12349", + }, }, - }, - ], + ], + }, }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "observer.geo.name": "harrisburg", + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "url.port": "5601", + }, }, - }, - ], + ], + }, }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "monitor.type": "http", + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "url.port": "443", + }, }, - }, - ], + ], + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "observer.geo.name": "harrisburg", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "monitor.type": "http", + }, }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "tags": "unsecured", - }, - }, - ], + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "tags": "unsecured", + }, }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "tags": "containers", - }, - }, - ], + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "tags": "containers", + }, }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match_phrase": Object { - "tags": "org:google", - }, - }, - ], + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "tags": "org:google", + }, }, - }, - ], + ], + }, }, - }, - ], + ], + }, }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "exists": Object { - "field": "monitor.ip", - }, - }, - ], + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "monitor.ip", + }, }, - }, - ], + ], + }, }, - }, - "locations": Array [], - "numTimes": 3, - "timespanRange": Object { - "from": "now-15m", - "to": "now", - }, - "timestampRange": Object { - "from": 1620821917000, - "to": "now", - }, - "uptimeEsClient": Object { - "baseESClient": [MockFunction], - "count": [Function], - "getSavedObjectsClient": [Function], - "search": [Function], - }, + ], }, - ] + } `); expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` @@ -519,13 +551,14 @@ describe('status check alert', () => { "lastCheckedAt": "foo date string", "lastResolvedAt": undefined, "lastTriggeredAt": "foo date string", - "latestErrorMessage": undefined, + "latestErrorMessage": "error message 1", "monitorId": "first", "monitorName": "first", "monitorType": "myType", - "monitorUrl": undefined, + "monitorUrl": "localhost:8080", "observerHostname": undefined, "observerLocation": "harrisburg", + "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1", "statusMessage": "down", }, ] @@ -567,67 +600,56 @@ describe('status check alert', () => { await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": Object { - "bool": Object { - "filter": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "monitor.type": "http", - }, - }, - ], + expect(mockGetter.mock.calls[0][0].filters).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "monitor.type": "http", + }, }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "exists": Object { - "field": "url.full", - }, - }, - ], + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "url.full", + }, }, - }, - ], + ], + }, }, - }, - "locations": Array [], - "numTimes": 20, - "timespanRange": Object { - "from": "now-30h", - "to": "now", - }, - "timestampRange": Object { - "from": 1620714817000, - "to": "now", - }, - "uptimeEsClient": Object { - "baseESClient": [MockFunction], - "count": [Function], - "getSavedObjectsClient": [Function], - "search": [Function], - }, + ], }, - ] + } `); + expect(mockGetter.mock.calls[0][0]).toEqual( + expect.objectContaining({ + locations: [], + numTimes: 20, + timespanRange: { + from: 'now-30h', + to: 'now', + }, + }) + ); }); it('supports availability checks', async () => { - expect.assertions(8); + expect.assertions(13); toISOStringSpy.mockImplementation(() => 'availability test'); const mockGetter: jest.Mock = jest.fn(); mockGetter.mockReturnValue([]); - const mockAvailability: jest.Mock = jest.fn(); - mockAvailability.mockReturnValue([ + const mockAvailabilityMonitors = [ { monitorId: 'foo', location: 'harrisburg', @@ -679,7 +701,9 @@ describe('status check alert', () => { url: 'https://no-name.co', }), }, - ]); + ]; + const mockAvailability: jest.Mock = jest.fn(); + mockAvailability.mockReturnValue(mockAvailabilityMonitors); const { server, libs, plugins } = bootstrapDependencies({ getMonitorAvailability: mockAvailability, getMonitorStatus: mockGetter, @@ -701,9 +725,14 @@ describe('status check alert', () => { shouldCheckAvailability: true, shouldCheckStatus: false, }); - const alertServices: AlertServicesMock = options.services; + const { + services: { alertWithLifecycle }, + } = options; const state = await alert.executor(options); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + mockAvailabilityMonitors.forEach((monitor) => { + expect(alertWithLifecycle).toBeCalledWith(mockAvailabilityAlertDocument(monitor)); + }); expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4); expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -722,6 +751,7 @@ describe('status check alert', () => { "monitorUrl": "https://foo.com", "observerHostname": undefined, "observerLocation": "harrisburg", + "reason": "Monitor Foo with url https://foo.com is below threshold with 99.28% availability expected is 99.34% from harrisburg. The latest error message is ", "statusMessage": "below threshold with 99.28% availability expected is 99.34%", }, ] @@ -731,48 +761,30 @@ describe('status check alert', () => { Array [ Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "message": "Monitor Foo with url https://foo.com is below threshold with 99.28% availability expected is 99.34% from harrisburg. The latest error message is ", - }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "message": "Monitor Foo with url https://foo.com is below threshold with 98.03% availability expected is 99.34% from fairbanks. The latest error message is ", - }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "message": "Monitor Unreliable with url https://unreliable.co is below threshold with 90.92% availability expected is 99.34% from fairbanks. The latest error message is ", - }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "message": "Monitor no-name with url https://no-name.co is below threshold with 90.92% availability expected is 99.34% from fairbanks. The latest error message is ", - }, ], ] `); expect(mockGetter).not.toHaveBeenCalled(); expect(mockAvailability).toHaveBeenCalledTimes(1); - expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"12349\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"5601\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"443\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", - "range": 35, - "rangeUnit": "d", - "threshold": "99.34", - "uptimeEsClient": Object { - "baseESClient": [MockFunction], - "count": [Function], - "getSavedObjectsClient": [Function], - "search": [Function], - }, - }, - ] - `); + expect(mockAvailability.mock.calls[0][0].filters).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"12349\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"5601\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"443\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}"` + ); + expect(mockAvailability.mock.calls[0][0]).toEqual( + expect.objectContaining({ + range: 35, + rangeUnit: 'd', + threshold: '99.34', + }) + ); expect(state).toMatchInlineSnapshot(` Object { "currentTriggerStarted": undefined, @@ -787,7 +799,7 @@ describe('status check alert', () => { }); it('supports availability checks with search', async () => { - expect.assertions(2); + expect.assertions(3); toISOStringSpy.mockImplementation(() => 'availability with search'); const mockGetter = jest.fn(); mockGetter.mockReturnValue([]); @@ -811,22 +823,16 @@ describe('status check alert', () => { await alert.executor(options); expect(mockAvailability).toHaveBeenCalledTimes(1); - expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"ur.port\\"}}],\\"minimum_should_match\\":1}}", - "range": 23, - "rangeUnit": "w", - "threshold": "90", - "uptimeEsClient": Object { - "baseESClient": [MockFunction], - "count": [Function], - "getSavedObjectsClient": [Function], - "search": [Function], - }, - }, - ] - `); + expect(mockAvailability.mock.calls[0][0].filters).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"ur.port\\"}}],\\"minimum_should_match\\":1}}"` + ); + expect(mockAvailability.mock.calls[0][0]).toEqual( + expect.objectContaining({ + range: 23, + rangeUnit: 'w', + threshold: '90', + }) + ); }); it('supports availability checks with no filter or search', async () => { @@ -841,12 +847,13 @@ describe('status check alert', () => { getIndexPattern: jest.fn(), }); const alert = statusCheckAlertFactory(server, libs, plugins); + const availability = { + range: 23, + rangeUnit: 'w', + threshold: '90', + }; const options = mockOptions({ - availability: { - range: 23, - rangeUnit: 'w', - threshold: '90', - }, + availability, shouldCheckAvailability: true, shouldCheckStatus: false, }); @@ -854,34 +861,20 @@ describe('status check alert', () => { await alert.executor(options); expect(mockAvailability).toHaveBeenCalledTimes(1); - expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filters": undefined, - "range": 23, - "rangeUnit": "w", - "threshold": "90", - "uptimeEsClient": Object { - "baseESClient": [MockFunction], - "count": [Function], - "getSavedObjectsClient": [Function], - "search": [Function], - }, - }, - ] - `); + expect(mockAvailability.mock.calls[0][0]).toEqual( + expect.objectContaining({ + filters: undefined, + range: availability.range, + rangeUnit: availability.rangeUnit, + threshold: availability.threshold, + }) + ); }); }); describe('alert factory', () => { // @ts-ignore - let alert: AlertType< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - 'xpack.uptime.alerts.actionGroups.monitorStatus' - >; + let alert: DefaultUptimeAlertInstance; beforeEach(() => { const { server, libs, plugins } = bootstrapDependencies(); @@ -982,7 +975,6 @@ describe('status check alert', () => { search: 'url.full: *', }, undefined, - undefined, { schedule: { interval: '60h' } } ); await alert.executor(options); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 364518bba720..249eaa33ec24 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -4,13 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import datemath from '@elastic/datemath'; import { min } from 'lodash'; +import datemath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import Mustache from 'mustache'; import { JsonObject } from '@kbn/common-utils'; -import { ActionGroupIdsOf } from '../../../../alerting/common'; import { UptimeAlertTypeFactory } from './types'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { @@ -19,16 +17,16 @@ import { GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; import { MONITOR_STATUS } from '../../../common/constants/alerts'; -import { updateState } from './common'; +import { updateState, generateAlertMessage } from './common'; import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations'; import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib'; import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability'; import { GetMonitorStatusResult } from '../requests/get_monitor_status'; import { UNNAMED_LOCATION } from '../../../common/constants'; -import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { MonitorStatusTranslations } from '../../../common/translations'; import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern'; -import { UMServerLibs, UptimeESClient } from '../lib'; +import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib'; +import { ActionGroupIdsOf } from '../../../../alerting/common'; export type ActionGroupIds = ActionGroupIdsOf; @@ -134,8 +132,8 @@ export const formatFilterString = async ( search ); -export const getMonitorSummary = (monitorInfo: Ping) => { - return { +export const getMonitorSummary = (monitorInfo: Ping, statusMessage: string) => { + const summary = { monitorUrl: monitorInfo.url?.full, monitorId: monitorInfo.monitor?.id, monitorName: monitorInfo.monitor?.name ?? monitorInfo.monitor?.id, @@ -144,16 +142,26 @@ export const getMonitorSummary = (monitorInfo: Ping) => { observerLocation: monitorInfo.observer?.geo?.name ?? UNNAMED_LOCATION, observerHostname: monitorInfo.agent?.name, }; + const reason = generateAlertMessage(MonitorStatusTranslations.defaultActionMessage, { + ...summary, + statusMessage, + }); + return { + ...summary, + reason, + }; }; -const generateMessageForOlderVersions = (fields: Record) => { - const messageTemplate = MonitorStatusTranslations.defaultActionMessage; - - // Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from - // {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}} - - return Mustache.render(messageTemplate, { state: { ...fields } }); -}; +export const getMonitorAlertDocument = (monitorSummary: Record) => ({ + 'monitor.id': monitorSummary.monitorId, + 'monitor.type': monitorSummary.monitorType, + 'monitor.name': monitorSummary.monitorName, + 'url.full': monitorSummary.monitorUrl, + 'observer.geo.name': monitorSummary.observerLocation, + 'error.message': monitorSummary.latestErrorMessage, + 'agent.name': monitorSummary.observerHostname, + reason: monitorSummary.reason, +}); export const getStatusMessage = ( downMonInfo?: Ping, @@ -194,7 +202,7 @@ export const getStatusMessage = ( return statusMessage + availabilityMessage; }; -const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { +export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { const normalizeText = (txt: string) => { // replace url and name special characters with - return txt.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); @@ -209,200 +217,204 @@ const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { return `${urlText}_${monIdByLoc}`; }; -export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => - uptimeAlertWrapper({ - id: 'xpack.uptime.alerts.monitorStatus', - name: i18n.translate('xpack.uptime.alerts.monitorStatus', { - defaultMessage: 'Uptime monitor status', - }), - validate: { - params: schema.object({ - availability: schema.maybe( - schema.object({ - range: schema.number(), - rangeUnit: schema.string(), - threshold: schema.string(), - }) - ), - filters: schema.maybe( - schema.oneOf([ - // deprecated - schema.object({ - 'monitor.type': schema.maybe(schema.arrayOf(schema.string())), - 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())), - tags: schema.maybe(schema.arrayOf(schema.string())), - 'url.port': schema.maybe(schema.arrayOf(schema.string())), - }), - schema.string(), - ]) - ), - // deprecated - locations: schema.maybe(schema.arrayOf(schema.string())), - numTimes: schema.number(), - search: schema.maybe(schema.string()), - shouldCheckStatus: schema.boolean(), - shouldCheckAvailability: schema.boolean(), - timerangeCount: schema.maybe(schema.number()), - timerangeUnit: schema.maybe(schema.string()), - // deprecated - timerange: schema.maybe( +export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ + id: 'xpack.uptime.alerts.monitorStatus', + producer: 'uptime', + name: i18n.translate('xpack.uptime.alerts.monitorStatus', { + defaultMessage: 'Uptime monitor status', + }), + validate: { + params: schema.object({ + availability: schema.maybe( + schema.object({ + range: schema.number(), + rangeUnit: schema.string(), + threshold: schema.string(), + }) + ), + filters: schema.maybe( + schema.oneOf([ + // deprecated schema.object({ - from: schema.string(), - to: schema.string(), - }) - ), - version: schema.maybe(schema.number()), - isAutoGenerated: schema.maybe(schema.boolean()), - }), + 'monitor.type': schema.maybe(schema.arrayOf(schema.string())), + 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())), + tags: schema.maybe(schema.arrayOf(schema.string())), + 'url.port': schema.maybe(schema.arrayOf(schema.string())), + }), + schema.string(), + ]) + ), + // deprecated + locations: schema.maybe(schema.arrayOf(schema.string())), + numTimes: schema.number(), + search: schema.maybe(schema.string()), + shouldCheckStatus: schema.boolean(), + shouldCheckAvailability: schema.boolean(), + timerangeCount: schema.maybe(schema.number()), + timerangeUnit: schema.maybe(schema.string()), + // deprecated + timerange: schema.maybe( + schema.object({ + from: schema.string(), + to: schema.string(), + }) + ), + version: schema.maybe(schema.number()), + isAutoGenerated: schema.maybe(schema.boolean()), + }), + }, + defaultActionGroupId: MONITOR_STATUS.id, + actionGroups: [ + { + id: MONITOR_STATUS.id, + name: MONITOR_STATUS.name, }, - defaultActionGroupId: MONITOR_STATUS.id, - actionGroups: [ + ], + actionVariables: { + context: [ { - id: MONITOR_STATUS.id, - name: MONITOR_STATUS.name, + name: 'message', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description', + { + defaultMessage: 'A generated message summarizing the currently down monitors', + } + ), + }, + { + name: 'downMonitorsWithGeo', + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description', + { + defaultMessage: + 'A generated summary that shows some or all of the monitors detected as "down" by the alert', + } + ), }, ], - actionVariables: { - context: [ - { - name: 'message', - description: i18n.translate( - 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description', - { - defaultMessage: 'A generated message summarizing the currently down monitors', - } - ), - }, - { - name: 'downMonitorsWithGeo', - description: i18n.translate( - 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description', - { - defaultMessage: - 'A generated summary that shows some or all of the monitors detected as "down" by the alert', - } - ), - }, - ], - state: [...commonMonitorStateI18, ...commonStateTranslations], + state: [...commonMonitorStateI18, ...commonStateTranslations], + }, + isExportable: true, + minimumLicenseRequired: 'basic', + async executor({ + params: rawParams, + state, + services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle }, + rule: { + schedule: { interval }, }, - minimumLicenseRequired: 'basic', - isExportable: true, - async executor({ - options: { - params: rawParams, - state, - services: { alertInstanceFactory }, - rule: { - schedule: { interval }, - }, - }, - uptimeEsClient, - }) { - const { - filters, - search, + }) { + const { + filters, + search, + numTimes, + timerangeCount, + timerangeUnit, + availability, + shouldCheckAvailability, + shouldCheckStatus, + isAutoGenerated, + timerange: oldVersionTimeRange, + } = rawParams; + const uptimeEsClient = createUptimeESClient({ + esClient: scopedClusterClient.asCurrentUser, + savedObjectsClient, + }); + + const filterString = await formatFilterString(uptimeEsClient, filters, search, libs); + + const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`; + + // Range filter for `monitor.timespan`, the range of time the ping is valid + const timespanRange = oldVersionTimeRange || { + from: `now-${timespanInterval}`, + to: 'now', + }; + + // Range filter for `@timestamp`, the time the document was indexed + const timestampRange = getTimestampRange({ + ruleScheduleLookback: `now-${interval}`, + timerangeLookback: timespanRange.from, + }); + + let downMonitorsByLocation: GetMonitorStatusResult[] = []; + + // if oldVersionTimeRange present means it's 7.7 format and + // after that shouldCheckStatus should be explicitly false + if (!(!oldVersionTimeRange && shouldCheckStatus === false)) { + downMonitorsByLocation = await libs.requests.getMonitorStatus({ + uptimeEsClient, + timespanRange, + timestampRange, numTimes, - timerangeCount, - timerangeUnit, - availability, - shouldCheckAvailability, - shouldCheckStatus, - isAutoGenerated, - timerange: oldVersionTimeRange, - } = rawParams; - const filterString = await formatFilterString(uptimeEsClient, filters, search, libs); - - const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`; - - // Range filter for `monitor.timespan`, the range of time the ping is valid - const timespanRange = oldVersionTimeRange || { - from: `now-${timespanInterval}`, - to: 'now', - }; - - // Range filter for `@timestamp`, the time the document was indexed - const timestampRange = getTimestampRange({ - ruleScheduleLookback: `now-${interval}`, - timerangeLookback: timespanRange.from, + locations: [], + filters: filterString, }); + } - let downMonitorsByLocation: GetMonitorStatusResult[] = []; - - // if oldVersionTimeRange present means it's 7.7 format and - // after that shouldCheckStatus should be explicitly false - if (!(!oldVersionTimeRange && shouldCheckStatus === false)) { - downMonitorsByLocation = await libs.requests.getMonitorStatus({ - uptimeEsClient, - timespanRange, - timestampRange, - numTimes, - locations: [], - filters: filterString, - }); - } - - if (isAutoGenerated) { - for (const monitorLoc of downMonitorsByLocation) { - const monitorInfo = monitorLoc.monitorInfo; - - const alertInstance = alertInstanceFactory( - getInstanceId(monitorInfo, monitorLoc.location) - ); - - const monitorSummary = getMonitorSummary(monitorInfo); - const statusMessage = getStatusMessage(monitorInfo); + if (isAutoGenerated) { + for (const monitorLoc of downMonitorsByLocation) { + const monitorInfo = monitorLoc.monitorInfo; - alertInstance.replaceState({ - ...state, - ...monitorSummary, - statusMessage, - ...updateState(state, true), - }); + const statusMessage = getStatusMessage(monitorInfo); + const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); - alertInstance.scheduleActions(MONITOR_STATUS.id); - } - return updateState(state, downMonitorsByLocation.length > 0); - } + const alert = alertWithLifecycle({ + id: getInstanceId(monitorInfo, monitorLoc.location), + fields: getMonitorAlertDocument(monitorSummary), + }); - let availabilityResults: GetMonitorAvailabilityResult[] = []; - if (shouldCheckAvailability) { - availabilityResults = await libs.requests.getMonitorAvailability({ - uptimeEsClient, - ...availability, - filters: JSON.stringify(filterString) || undefined, + alert.replaceState({ + ...state, + ...monitorSummary, + statusMessage, + ...updateState(state, true), }); + + alert.scheduleActions(MONITOR_STATUS.id); } + return updateState(state, downMonitorsByLocation.length > 0); + } - const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults); + let availabilityResults: GetMonitorAvailabilityResult[] = []; + if (shouldCheckAvailability) { + availabilityResults = await libs.requests.getMonitorAvailability({ + uptimeEsClient, + ...availability, + filters: JSON.stringify(filterString) || undefined, + }); + } - mergedIdsByLoc.forEach((monIdByLoc) => { - const availMonInfo = availabilityResults.find( - ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc - ); + const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults); - const downMonInfo = downMonitorsByLocation.find( - ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc - )?.monitorInfo; + mergedIdsByLoc.forEach((monIdByLoc) => { + const availMonInfo = availabilityResults.find( + ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc + ); - const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!; + const downMonInfo = downMonitorsByLocation.find( + ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc + )?.monitorInfo; - const monitorSummary = getMonitorSummary(monitorInfo); - const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability); + const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!; - const alertInstance = alertInstanceFactory(getInstanceId(monitorInfo, monIdByLoc)); + const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability); + const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); - alertInstance.replaceState({ - ...updateState(state, true), - ...monitorSummary, - statusMessage, - }); + const alert = alertWithLifecycle({ + id: getInstanceId(monitorInfo, monIdByLoc), + fields: getMonitorAlertDocument(monitorSummary), + }); - alertInstance.scheduleActions(MONITOR_STATUS.id, { - message: generateMessageForOlderVersions({ ...monitorSummary, statusMessage }), - }); + alert.replaceState({ + ...updateState(state, true), + ...monitorSummary, + statusMessage, }); - return updateState(state, downMonitorsByLocation.length > 0); - }, - }); + alert.scheduleActions(MONITOR_STATUS.id); + }); + + return updateState(state, downMonitorsByLocation.length > 0); + }, +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts new file mode 100644 index 000000000000..8bbf20f3a64a --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts @@ -0,0 +1,81 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { UMServerLibs } from '../../lib'; +import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters'; +import type { UptimeRouter } from '../../../types'; +import type { RuleDataClient } from '../../../../../rule_registry/server'; +import { getUptimeESMockClient } from '../../requests/helper'; +import { alertsMock } from '../../../../../alerting/server/mocks'; +import { DynamicSettings } from '../../../../common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; + +/** + * The alert takes some dependencies as parameters; these are things like + * kibana core services and plugins. This function helps reduce the amount of + * boilerplate required. + * @param customRequests client tests can use this paramter to provide their own request mocks, + * so we don't have to mock them all for each test. + */ +export const bootstrapDependencies = (customRequests?: any, customPlugins: any = {}) => { + const router = {} as UptimeRouter; + // these server/libs parameters don't have any functionality, which is fine + // because we aren't testing them here + const server: UptimeCoreSetup = { router }; + const plugins: UptimeCorePlugins = customPlugins as any; + const libs: UMServerLibs = { requests: {} } as UMServerLibs; + libs.requests = { ...libs.requests, ...customRequests }; + return { server, libs, plugins }; +}; + +export const createRuleTypeMocks = ( + dynamicCertSettings: { + certAgeThreshold: DynamicSettings['certAgeThreshold']; + certExpirationThreshold: DynamicSettings['certExpirationThreshold']; + } = { + certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, + certExpirationThreshold: DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, + } +) => { + const loggerMock = ({ + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + } as unknown) as Logger; + + const scheduleActions = jest.fn(); + const replaceState = jest.fn(); + + const services = { + ...getUptimeESMockClient(), + ...alertsMock.createAlertServices(), + alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }), + logger: loggerMock, + }; + + return { + dependencies: { + logger: loggerMock, + ruleDataClient: ({ + getReader: () => { + return { + search: jest.fn(), + }; + }, + getWriter: () => { + return { + bulk: jest.fn(), + }; + }, + } as unknown) as RuleDataClient, + }, + services, + scheduleActions, + replaceState, + }; +}; diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts index a77fe10f0b9a..2536056363dd 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts @@ -4,52 +4,180 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import moment from 'moment'; -import { getCertSummary } from './tls'; -import { Cert } from '../../../common/runtime_types'; + +import { tlsAlertFactory, getCertSummary, DEFAULT_SIZE } from './tls'; +import { TLS } from '../../../common/constants/alerts'; +import { CertResult, DynamicSettings } from '../../../common/runtime_types'; +import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; + +import { savedObjectsAdapter, UMSavedObjectsAdapter } from '../saved_objects'; + +/** + * This function aims to provide an easy way to give mock props that will + * reduce boilerplate for tests. + * @param params the params received at alert creation time + * @param state the state the alert maintains + */ +const mockOptions = ( + dynamicCertSettings?: { + certExpirationThreshold: DynamicSettings['certExpirationThreshold']; + certAgeThreshold: DynamicSettings['certAgeThreshold']; + }, + state = {} +): any => { + const { services } = createRuleTypeMocks(dynamicCertSettings); + const params = { + timerange: { from: 'now-15m', to: 'now' }, + }; + + return { + params, + state, + services, + }; +}; + +const mockCertResult: CertResult = { + certs: [ + { + not_after: '2020-07-16T03:15:39.000Z', + not_before: '2019-07-24T03:15:39.000Z', + issuer: 'Sample issuer', + common_name: 'Common-One', + monitors: [{ name: 'monitor-one', id: 'monitor1' }], + sha256: 'abc', + }, + { + not_after: '2020-07-18T03:15:39.000Z', + not_before: '2019-07-20T03:15:39.000Z', + issuer: 'Sample issuer', + common_name: 'Common-Two', + monitors: [{ name: 'monitor-two', id: 'monitor2' }], + sha256: 'bcd', + }, + { + not_after: '2020-07-19T03:15:39.000Z', + not_before: '2019-07-22T03:15:39.000Z', + issuer: 'Sample issuer', + common_name: 'Common-Three', + monitors: [{ name: 'monitor-three', id: 'monitor3' }], + sha256: 'cde', + }, + { + not_after: '2020-07-25T03:15:39.000Z', + not_before: '2019-07-25T03:15:39.000Z', + issuer: 'Sample issuer', + common_name: 'Common-Four', + monitors: [{ name: 'monitor-four', id: 'monitor4' }], + sha256: 'def', + }, + ], + total: 4, +}; describe('tls alert', () => { + let toISOStringSpy: jest.SpyInstance; + let savedObjectsAdapterSpy: jest.SpyInstance< + ReturnType + >; + const mockDate = 'date'; + beforeAll(() => { + Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z')); + }); + + describe('alert executor', () => { + beforeEach(() => { + toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString'); + savedObjectsAdapterSpy = jest.spyOn(savedObjectsAdapter, 'getUptimeDynamicSettings'); + }); + + it('triggers when aging or expiring alerts are found', async () => { + toISOStringSpy.mockImplementation(() => mockDate); + const mockGetter: jest.Mock = jest.fn(); + + mockGetter.mockReturnValue(mockCertResult); + const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter }); + const alert = tlsAlertFactory(server, libs, plugins); + const options = mockOptions(); + const { + services: { alertWithLifecycle }, + } = options; + await alert.executor(options); + expect(mockGetter).toHaveBeenCalledTimes(1); + expect(alertWithLifecycle).toHaveBeenCalledTimes(4); + mockCertResult.certs.forEach((cert) => { + expect(alertWithLifecycle).toBeCalledWith({ + fields: expect.objectContaining({ + 'tls.server.x509.subject.common_name': cert.common_name, + 'tls.server.x509.issuer.common_name': cert.issuer, + 'tls.server.x509.not_after': cert.not_after, + 'tls.server.x509.not_before': cert.not_before, + 'tls.server.hash.sha256': cert.sha256, + }), + id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`, + }); + }); + expect(mockGetter).toBeCalledWith( + expect.objectContaining({ + from: DEFAULT_FROM, + to: DEFAULT_TO, + index: 0, + size: DEFAULT_SIZE, + notValidAfter: `now+${DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold}d`, + notValidBefore: `now-${DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold}d`, + sortBy: 'common_name', + direction: 'desc', + }) + ); + const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4); + mockCertResult.certs.forEach((cert) => { + expect(alertInstanceMock.replaceState).toBeCalledWith( + expect.objectContaining({ + commonName: cert.common_name, + issuer: cert.issuer, + status: 'expired', + }) + ); + }); + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4); + expect(alertInstanceMock.scheduleActions).toBeCalledWith(TLS.id); + }); + + it('handles dynamic settings for aging or expiration threshold', async () => { + toISOStringSpy.mockImplementation(() => mockDate); + const certSettings = { + certAgeThreshold: 10, + certExpirationThreshold: 5, + heartbeatIndices: 'heartbeat-*', + defaultConnectors: [], + }; + savedObjectsAdapterSpy.mockImplementation(() => certSettings); + const mockGetter: jest.Mock = jest.fn(); + + mockGetter.mockReturnValue(mockCertResult); + const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter }); + const alert = tlsAlertFactory(server, libs, plugins); + const options = mockOptions(); + await alert.executor(options); + expect(mockGetter).toHaveBeenCalledTimes(1); + expect(mockGetter).toBeCalledWith( + expect.objectContaining({ + notValidAfter: `now+${certSettings.certExpirationThreshold}d`, + notValidBefore: `now-${certSettings.certAgeThreshold}d`, + }) + ); + }); + }); + describe('getCertSummary', () => { - let mockCerts: Cert[]; let diffSpy: jest.SpyInstance; beforeEach(() => { diffSpy = jest.spyOn(moment.prototype, 'diff'); - mockCerts = [ - { - not_after: '2020-07-16T03:15:39.000Z', - not_before: '2019-07-24T03:15:39.000Z', - common_name: 'Common-One', - monitors: [{ name: 'monitor-one', id: 'monitor1' }], - sha256: 'abc', - issuer: 'Cloudflare Inc ECC CA-3', - }, - { - not_after: '2020-07-18T03:15:39.000Z', - not_before: '2019-07-20T03:15:39.000Z', - common_name: 'Common-Two', - monitors: [{ name: 'monitor-two', id: 'monitor2' }], - sha256: 'bcd', - issuer: 'Cloudflare Inc ECC CA-3', - }, - { - not_after: '2020-07-19T03:15:39.000Z', - not_before: '2019-07-22T03:15:39.000Z', - common_name: 'Common-Three', - monitors: [{ name: 'monitor-three', id: 'monitor3' }], - sha256: 'cde', - issuer: 'Cloudflare Inc ECC CA-3', - }, - { - not_after: '2020-07-25T03:15:39.000Z', - not_before: '2019-07-25T03:15:39.000Z', - common_name: 'Common-Four', - monitors: [{ name: 'monitor-four', id: 'monitor4' }], - sha256: 'def', - issuer: 'Cloudflare Inc ECC CA-3', - }, - ]; }); afterEach(() => { @@ -59,13 +187,13 @@ describe('tls alert', () => { it('handles positive diffs for expired certs appropriately', () => { diffSpy.mockReturnValueOnce(900); const result = getCertSummary( - mockCerts[0], + mockCertResult.certs[0], new Date('2020-07-20T05:00:00.000Z').valueOf(), new Date('2019-03-01T00:00:00.000Z').valueOf() ); expect(result).toEqual({ - commonName: mockCerts[0].common_name, - issuer: mockCerts[0].issuer, + commonName: mockCertResult.certs[0].common_name, + issuer: mockCertResult.certs[0].issuer, summary: 'expired on Jul 15, 2020 EDT, 900 days ago.', status: 'expired', }); @@ -74,13 +202,13 @@ describe('tls alert', () => { it('handles positive diffs for agining certs appropriately', () => { diffSpy.mockReturnValueOnce(702); const result = getCertSummary( - mockCerts[0], + mockCertResult.certs[0], new Date('2020-07-01T12:00:00.000Z').valueOf(), new Date('2019-09-01T03:00:00.000Z').valueOf() ); expect(result).toEqual({ - commonName: mockCerts[0].common_name, - issuer: mockCerts[0].issuer, + commonName: mockCertResult.certs[0].common_name, + issuer: mockCertResult.certs[0].issuer, summary: 'valid since Jul 23, 2019 EDT, 702 days ago.', status: 'becoming too old', }); @@ -89,13 +217,13 @@ describe('tls alert', () => { it('handles negative diff values appropriately for aging certs', () => { diffSpy.mockReturnValueOnce(-90); const result = getCertSummary( - mockCerts[0], + mockCertResult.certs[0], new Date('2020-07-01T12:00:00.000Z').valueOf(), new Date('2019-09-01T03:00:00.000Z').valueOf() ); expect(result).toEqual({ - commonName: mockCerts[0].common_name, - issuer: mockCerts[0].issuer, + commonName: mockCertResult.certs[0].common_name, + issuer: mockCertResult.certs[0].issuer, summary: 'invalid until Jul 23, 2019 EDT, 90 days from now.', status: 'invalid', }); @@ -106,13 +234,13 @@ describe('tls alert', () => { // negative days are in the future, positive days are in the past .mockReturnValueOnce(-96); const result = getCertSummary( - mockCerts[0], + mockCertResult.certs[0], new Date('2020-07-20T05:00:00.000Z').valueOf(), new Date('2019-03-01T00:00:00.000Z').valueOf() ); expect(result).toEqual({ - commonName: mockCerts[0].common_name, - issuer: mockCerts[0].issuer, + commonName: mockCertResult.certs[0].common_name, + issuer: mockCertResult.certs[0].issuer, summary: 'expires on Jul 15, 2020 EDT in 96 days.', status: 'expiring', }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 09f5e2fe0f6d..8056fe210bf5 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -8,18 +8,22 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { UptimeAlertTypeFactory } from './types'; -import { updateState } from './common'; +import { updateState, generateAlertMessage } from './common'; import { TLS } from '../../../common/constants/alerts'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; -import { uptimeAlertWrapper } from './uptime_alert_wrapper'; +import { TlsTranslations } from '../../../common/translations'; + import { ActionGroupIdsOf } from '../../../../alerting/common'; +import { savedObjectsAdapter } from '../saved_objects'; +import { createUptimeESClient } from '../lib'; + export type ActionGroupIds = ActionGroupIdsOf; -const DEFAULT_SIZE = 20; +export const DEFAULT_SIZE = 20; interface TlsAlertState { commonName: string; @@ -93,78 +97,92 @@ export const getCertSummary = ( }; }; -export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => - uptimeAlertWrapper({ - id: 'xpack.uptime.alerts.tlsCertificate', - name: tlsTranslations.alertFactoryName, - validate: { - params: schema.object({}), - }, - defaultActionGroupId: TLS.id, - actionGroups: [ - { - id: TLS.id, - name: TLS.name, - }, - ], - actionVariables: { - context: [], - state: [...tlsTranslations.actionVariables, ...commonStateTranslations], +export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ + id: 'xpack.uptime.alerts.tlsCertificate', + producer: 'uptime', + name: tlsTranslations.alertFactoryName, + validate: { + params: schema.object({}), + }, + defaultActionGroupId: TLS.id, + actionGroups: [ + { + id: TLS.id, + name: TLS.name, }, - minimumLicenseRequired: 'basic', - isExportable: true, - async executor({ options, dynamicSettings, uptimeEsClient }) { - const { - services: { alertInstanceFactory }, - state, - } = options; - - const { certs, total }: CertResult = await libs.requests.getCerts({ - uptimeEsClient, - from: DEFAULT_FROM, - to: DEFAULT_TO, - index: 0, - size: DEFAULT_SIZE, - notValidAfter: `now+${ - dynamicSettings?.certExpirationThreshold ?? - DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold - }d`, - notValidBefore: `now-${ - dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold - }d`, - sortBy: 'common_name', - direction: 'desc', - }); - - const foundCerts = total > 0; - - if (foundCerts) { - certs.forEach((cert) => { - const absoluteExpirationThreshold = moment() - .add( - dynamicSettings.certExpirationThreshold ?? - DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, - 'd' - ) - .valueOf(); - const absoluteAgeThreshold = moment() - .subtract( - dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, - 'd' - ) - .valueOf(); - const alertInstance = alertInstanceFactory( - `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}` - ); - const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold); - alertInstance.replaceState({ - ...updateState(state, foundCerts), - ...summary, - }); - alertInstance.scheduleActions(TLS.id); + ], + actionVariables: { + context: [], + state: [...tlsTranslations.actionVariables, ...commonStateTranslations], + }, + isExportable: true, + minimumLicenseRequired: 'basic', + async executor({ + services: { alertWithLifecycle, savedObjectsClient, scopedClusterClient }, + state, + }) { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); + + const uptimeEsClient = createUptimeESClient({ + esClient: scopedClusterClient.asCurrentUser, + savedObjectsClient, + }); + + const { certs, total }: CertResult = await libs.requests.getCerts({ + uptimeEsClient, + from: DEFAULT_FROM, + to: DEFAULT_TO, + index: 0, + size: DEFAULT_SIZE, + notValidAfter: `now+${ + dynamicSettings?.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold + }d`, + notValidBefore: `now-${ + dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold + }d`, + sortBy: 'common_name', + direction: 'desc', + }); + + const foundCerts = total > 0; + + if (foundCerts) { + certs.forEach((cert) => { + const absoluteExpirationThreshold = moment() + .add( + dynamicSettings.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, + 'd' + ) + .valueOf(); + const absoluteAgeThreshold = moment() + .subtract( + dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, + 'd' + ) + .valueOf(); + const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold); + + const alertInstance = alertWithLifecycle({ + id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`, + fields: { + 'tls.server.x509.subject.common_name': cert.common_name, + 'tls.server.x509.issuer.common_name': cert.issuer, + 'tls.server.x509.not_after': cert.not_after, + 'tls.server.x509.not_before': cert.not_before, + 'tls.server.hash.sha256': cert.sha256, + reason: generateAlertMessage(TlsTranslations.defaultActionMessage, summary), + }, }); - } + alertInstance.replaceState({ + ...updateState(state, foundCerts), + ...summary, + }); + alertInstance.scheduleActions(TLS.id); + }); + } - return updateState(state, foundCerts); - }, - }); + return updateState(state, foundCerts); + }, +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts index 5bf91b7c5486..812925f22b24 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts @@ -14,11 +14,18 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; -import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { ActionGroupIdsOf } from '../../../../alerting/common'; +import { AlertInstanceContext } from '../../../../alerting/common'; +import { AlertInstance } from '../../../../alerting/server'; + +import { savedObjectsAdapter } from '../saved_objects'; +import { createUptimeESClient } from '../lib'; + export type ActionGroupIds = ActionGroupIdsOf; +type TLSAlertInstance = AlertInstance, AlertInstanceContext, ActionGroupIds>; + const DEFAULT_SIZE = 20; interface TlsAlertState { @@ -84,74 +91,78 @@ export const getCertSummary = ( }; }; -export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_server, libs) => - uptimeAlertWrapper({ - id: 'xpack.uptime.alerts.tls', - name: tlsTranslations.legacyAlertFactoryName, - validate: { - params: schema.object({}), +export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ + id: 'xpack.uptime.alerts.tls', + producer: 'uptime', + name: tlsTranslations.legacyAlertFactoryName, + validate: { + params: schema.object({}), + }, + defaultActionGroupId: TLS_LEGACY.id, + actionGroups: [ + { + id: TLS_LEGACY.id, + name: TLS_LEGACY.name, }, - defaultActionGroupId: TLS_LEGACY.id, - actionGroups: [ - { - id: TLS_LEGACY.id, - name: TLS_LEGACY.name, - }, - ], - actionVariables: { - context: [], - state: [...tlsTranslations.actionVariables, ...commonStateTranslations], - }, - minimumLicenseRequired: 'basic', - isExportable: true, - async executor({ options, dynamicSettings, uptimeEsClient }) { - const { - services: { alertInstanceFactory }, - state, - } = options; - - const { certs, total }: CertResult = await libs.requests.getCerts({ - uptimeEsClient, - from: DEFAULT_FROM, - to: DEFAULT_TO, - index: 0, - size: DEFAULT_SIZE, - notValidAfter: `now+${ - dynamicSettings?.certExpirationThreshold ?? - DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold - }d`, - notValidBefore: `now-${ - dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold - }d`, - sortBy: 'common_name', - direction: 'desc', + ], + actionVariables: { + context: [], + state: [...tlsTranslations.actionVariables, ...commonStateTranslations], + }, + isExportable: true, + minimumLicenseRequired: 'basic', + async executor({ + services: { alertInstanceFactory, scopedClusterClient, savedObjectsClient }, + state, + }) { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); + + const uptimeEsClient = createUptimeESClient({ + esClient: scopedClusterClient.asCurrentUser, + savedObjectsClient, + }); + const { certs, total }: CertResult = await libs.requests.getCerts({ + uptimeEsClient, + from: DEFAULT_FROM, + to: DEFAULT_TO, + index: 0, + size: DEFAULT_SIZE, + notValidAfter: `now+${ + dynamicSettings?.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold + }d`, + notValidBefore: `now-${ + dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold + }d`, + sortBy: 'common_name', + direction: 'desc', + }); + + const foundCerts = total > 0; + + if (foundCerts) { + const absoluteExpirationThreshold = moment() + .add( + dynamicSettings.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, + 'd' + ) + .valueOf(); + const absoluteAgeThreshold = moment() + .subtract( + dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, + 'd' + ) + .valueOf(); + const alertInstance: TLSAlertInstance = alertInstanceFactory(TLS_LEGACY.id); + const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); + alertInstance.replaceState({ + ...updateState(state, foundCerts), + ...summary, }); + alertInstance.scheduleActions(TLS_LEGACY.id); + } - const foundCerts = total > 0; - - if (foundCerts) { - const absoluteExpirationThreshold = moment() - .add( - dynamicSettings.certExpirationThreshold ?? - DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, - 'd' - ) - .valueOf(); - const absoluteAgeThreshold = moment() - .subtract( - dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, - 'd' - ) - .valueOf(); - const alertInstance = alertInstanceFactory(TLS_LEGACY.id); - const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); - alertInstance.replaceState({ - ...updateState(state, foundCerts), - ...summary, - }); - alertInstance.scheduleActions(TLS_LEGACY.id); - } - - return updateState(state, foundCerts); - }, - }); + return updateState(state, foundCerts); + }, +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index 6f9ca42e54ad..28f9eba7ab38 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -4,21 +4,27 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters'; import { UMServerLibs } from '../lib'; -import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../alerting/server'; +import { AlertTypeWithExecutor, LifecycleAlertService } from '../../../../rule_registry/server'; +import { AlertInstanceContext } from '../../../../alerting/common'; -export type UptimeAlertTypeParam = Record; -export type UptimeAlertTypeState = Record; -export type UptimeAlertTypeFactory = ( +/** + * Because all of our types are presumably going to list the `producer` as `'uptime'`, + * we should just omit this field from the returned value to simplify the returned alert type. + * + * When we register all the alerts we can inject this field. + */ +export type DefaultUptimeAlertInstance = AlertTypeWithExecutor< + Record, + AlertInstanceContext, + { + alertWithLifecycle: LifecycleAlertService; + } +>; + +export type UptimeAlertTypeFactory = ( server: UptimeCoreSetup, libs: UMServerLibs, plugins: UptimeCorePlugins -) => AlertType< - UptimeAlertTypeParam, - UptimeAlertTypeState, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIds ->; +) => DefaultUptimeAlertInstance; diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts deleted file mode 100644 index 654f99cb0265..000000000000 --- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts +++ /dev/null @@ -1,68 +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 { SavedObjectsClientContract } from 'kibana/server'; -import { - AlertExecutorOptions, - AlertInstanceState, - AlertInstanceContext, -} from '../../../../alerting/server'; -import { savedObjectsAdapter } from '../saved_objects'; -import { DynamicSettings } from '../../../common/runtime_types'; -import { createUptimeESClient, UptimeESClient } from '../lib'; -import { UptimeAlertTypeFactory, UptimeAlertTypeParam, UptimeAlertTypeState } from './types'; - -export interface UptimeAlertType - extends Omit>, 'executor' | 'producer'> { - executor: ({ - options, - uptimeEsClient, - dynamicSettings, - }: { - options: AlertExecutorOptions< - UptimeAlertTypeParam, - UptimeAlertTypeState, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIds - >; - uptimeEsClient: UptimeESClient; - dynamicSettings: DynamicSettings; - savedObjectsClient: SavedObjectsClientContract; - }) => Promise; -} - -export const uptimeAlertWrapper = ( - uptimeAlert: UptimeAlertType -) => ({ - ...uptimeAlert, - producer: 'uptime', - executor: async ( - options: AlertExecutorOptions< - UptimeAlertTypeParam, - UptimeAlertTypeState, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIds - > - ) => { - const { - services: { scopedClusterClient: esClient, savedObjectsClient }, - } = options; - - const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( - options.services.savedObjectsClient - ); - - const uptimeEsClient = createUptimeESClient({ - esClient: esClient.asCurrentUser, - savedObjectsClient, - }); - - return uptimeAlert.executor({ options, dynamicSettings, uptimeEsClient, savedObjectsClient }); - }, -}); diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index c0fecf6f19af..5ef5e17d4e33 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -4,30 +4,97 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { once } from 'lodash'; import { PluginInitializerContext, CoreStart, CoreSetup, Plugin as PluginType, ISavedObjectsRepository, + Logger, } from '../../../../src/core/server'; +import { uptimeRuleFieldMap } from '../common/rules/uptime_rule_field_map'; import { initServerWithKibana } from './kibana.index'; import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters'; import { umDynamicSettings } from './lib/saved_objects'; +import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; +import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; + +export type UptimeRuleRegistry = ReturnType['ruleRegistry']; export class Plugin implements PluginType { private savedObjectsClient?: ISavedObjectsRepository; + private initContext: PluginInitializerContext; + private logger?: Logger; - constructor(_initializerContext: PluginInitializerContext) {} + constructor(_initializerContext: PluginInitializerContext) { + this.initContext = _initializerContext; + } public setup(core: CoreSetup, plugins: UptimeCorePlugins) { - initServerWithKibana({ router: core.http.createRouter() }, plugins); + this.logger = this.initContext.logger.get(); + const { ruleDataService } = plugins.ruleRegistry; + + const ready = once(async () => { + const componentTemplateName = ruleDataService.getFullAssetName('synthetics-mappings'); + const alertsIndexPattern = ruleDataService.getFullAssetName('observability.synthetics*'); + + if (!ruleDataService.isWriteEnabled()) { + return; + } + + await ruleDataService.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: mappingFromFieldMap(uptimeRuleFieldMap), + }, + }, + }); + + await ruleDataService.createOrUpdateIndexTemplate({ + name: ruleDataService.getFullAssetName('synthetics-index-template'), + body: { + index_patterns: [alertsIndexPattern], + composed_of: [ + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + componentTemplateName, + ], + }, + }); + + await ruleDataService.updateIndexMappingsMatchingPattern(alertsIndexPattern); + }); + + // initialize eagerly + const initializeRuleDataTemplatesPromise = ready().catch((err) => { + this.logger!.error(err); + }); + + const ruleDataClient = ruleDataService.getRuleDataClient( + 'synthetics', + ruleDataService.getFullAssetName('observability.synthetics'), + () => initializeRuleDataTemplatesPromise + ); + + initServerWithKibana( + { router: core.http.createRouter() }, + plugins, + ruleDataClient, + this.logger + ); core.savedObjects.registerType(umDynamicSettings); KibanaTelemetryAdapter.registerUsageCollector( plugins.usageCollection, () => this.savedObjectsClient ); + + return { + ruleRegistry: ruleDataClient, + }; } public start(core: CoreStart, _plugins: any) { diff --git a/x-pack/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts index 39e5c9bff202..f52b4a806335 100644 --- a/x-pack/plugins/uptime/server/uptime_server.ts +++ b/x-pack/plugins/uptime/server/uptime_server.ts @@ -5,21 +5,47 @@ * 2.0. */ +import { Logger } from 'kibana/server'; +import { createLifecycleRuleTypeFactory, RuleDataClient } from '../../rule_registry/server'; import { UMServerLibs } from './lib/lib'; import { createRouteWithAuth, restApiRoutes, uptimeRouteWrapper } from './rest_api'; import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters'; -import { uptimeAlertTypeFactories } from './lib/alerts'; + +import { statusCheckAlertFactory } from './lib/alerts/status_check'; +import { tlsAlertFactory } from './lib/alerts/tls'; +import { tlsLegacyAlertFactory } from './lib/alerts/tls_legacy'; +import { durationAnomalyAlertFactory } from './lib/alerts/duration_anomaly'; export const initUptimeServer = ( server: UptimeCoreSetup, libs: UMServerLibs, - plugins: UptimeCorePlugins + plugins: UptimeCorePlugins, + ruleDataClient: RuleDataClient, + logger: Logger ) => { restApiRoutes.forEach((route) => libs.framework.registerRoute(uptimeRouteWrapper(createRouteWithAuth(libs, route))) ); - uptimeAlertTypeFactories.forEach((alertTypeFactory) => - plugins.alerting.registerType(alertTypeFactory(server, libs, plugins)) - ); + const { + alerting: { registerType }, + } = plugins; + + const statusAlert = statusCheckAlertFactory(server, libs, plugins); + const tlsLegacyAlert = tlsLegacyAlertFactory(server, libs, plugins); + const tlsAlert = tlsAlertFactory(server, libs, plugins); + const durationAlert = durationAnomalyAlertFactory(server, libs, plugins); + + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + ruleDataClient, + logger, + }); + + registerType(createLifecycleRuleType(statusAlert)); + registerType(createLifecycleRuleType(tlsAlert)); + registerType(createLifecycleRuleType(durationAlert)); + + /* TLS Legacy rule supported at least through 8.0. + * Not registered with RAC */ + registerType(tlsLegacyAlert); }; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 19c3543ce4d0..9e527835231b 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -88,7 +88,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/saved_object_tagging/api_integration/security_and_spaces/config.ts'), require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), require.resolve('../test/examples/config.ts'), - require.resolve('../test/cloud_integration/config.ts'), ]; require('../../src/setup_node_env'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 29f2ed40be79..23f7f337aa81 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -103,6 +103,50 @@ export default function alertTests({ getService }: FtrProviderContext) { } }); + it('runs correctly: use epoch millis - threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + + await createAlert({ + name: 'never fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '<', + threshold: [0], + timeField: 'date_epoch_millis', + }); + + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '>', + threshold: [-1], + timeField: 'date_epoch_millis', + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } else { + expect(previousTimestamp).not.to.be.empty(); + } + } + }); + it('runs correctly with query: threshold on hit count < >', async () => { // write documents from now to the future end date in groups createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 4e3740a1ccb1..0677acd500c1 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -376,6 +376,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.status": Array [ "open", ], + "kibana.space_ids": Array [ + "default", + ], "processor.event": Array [ "transaction", ], @@ -449,6 +452,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.status": Array [ "open", ], + "kibana.space_ids": Array [ + "default", + ], "processor.event": Array [ "transaction", ], @@ -556,6 +562,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.status": Array [ "closed", ], + "kibana.space_ids": Array [ + "default", + ], "processor.event": Array [ "transaction", ], diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts index cc8f48fb5894..bbb2097f6301 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts @@ -26,7 +26,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const getRequestBody = () => { const partialSearchRequest: PartialSearchRequest = { params: { - index: 'apm-*', environment: 'ENVIRONMENT_ALL', start: '2020', end: '2021', @@ -141,7 +140,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when( 'Correlations latency_ml with data and opbeans-node args', - { config: 'trial', archives: ['ml_8.0.0'] }, + { config: 'trial', archives: ['8.0.0'] }, () => { // putting this into a single `it` because the responses depend on each other it('queries the search strategy and returns results', async () => { @@ -235,30 +234,30 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { rawResponse: finalRawResponse } = followUpResult; expect(typeof finalRawResponse?.took).to.be('number'); - expect(finalRawResponse?.percentileThresholdValue).to.be(1404927.875); + expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); expect(finalRawResponse?.overallHistogram.length).to.be(101); expect(finalRawResponse?.values.length).to.eql( - 1, - `Expected 1 identified correlations, got ${finalRawResponse?.values.length}.` + 13, + `Expected 13 identified correlations, got ${finalRawResponse?.values.length}.` ); expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ - 'Fetched 95th percentile value of 1404927.875 based on 989 documents.', + 'Fetched 95th percentile value of 1309695.875 based on 1244 documents.', 'Loaded histogram range steps.', 'Loaded overall histogram chart data.', 'Loaded percentiles.', - 'Identified 67 fieldCandidates.', - 'Identified 339 fieldValuePairs.', - 'Loaded fractions and totalDocCount of 989.', - 'Identified 1 significant correlations out of 339 field/value pairs.', + 'Identified 69 fieldCandidates.', + 'Identified 379 fieldValuePairs.', + 'Loaded fractions and totalDocCount of 1244.', + 'Identified 13 significant correlations out of 379 field/value pairs.', ]); const correlation = finalRawResponse?.values[0]; expect(typeof correlation).to.be('object'); expect(correlation?.field).to.be('transaction.result'); expect(correlation?.value).to.be('success'); - expect(correlation?.correlation).to.be(0.37418510688551887); - expect(correlation?.ksTest).to.be(1.1238496968312214e-10); + expect(correlation?.correlation).to.be(0.6275246559191225); + expect(correlation?.ksTest).to.be(4.806503252860024e-13); expect(correlation?.histogram.length).to.be(101); }); } diff --git a/x-pack/test/cloud_integration/tests/fullstory.ts b/x-pack/test/cloud_integration/tests/fullstory.ts index 5c328d27cc52..1cdad719e94e 100644 --- a/x-pack/test/cloud_integration/tests/fullstory.ts +++ b/x-pack/test/cloud_integration/tests/fullstory.ts @@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); describe('Cloud FullStory integration', function () { - this.tags('ciGroup13'); before(async () => { // Create role mapping so user gets superuser access await getService('esSupertest') diff --git a/x-pack/test/functional/apps/apm/correlations/index.ts b/x-pack/test/functional/apps/apm/correlations/index.ts new file mode 100644 index 000000000000..ae5f594e5440 --- /dev/null +++ b/x-pack/test/functional/apps/apm/correlations/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('correlations', function () { + this.tags('skipFirefox'); + loadTestFile(require.resolve('./latency_correlations')); + }); +} diff --git a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts new file mode 100644 index 000000000000..bc06b7299363 --- /dev/null +++ b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const retry = getService('retry'); + const spacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + const testData = { serviceName: 'opbeans-go' }; + + describe('latency correlations', () => { + describe('space with no features disabled', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows apm navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); + expect(navLinks).to.contain('APM'); + }); + + it('can navigate to APM app', async () => { + await PageObjects.common.navigateToApp('apm'); + + await retry.try(async () => { + await testSubjects.existOrFail('apmMainContainer', { + timeout: 10000, + }); + + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + expect(apmMainContainerTextItems).to.contain('No services found'); + }); + }); + + it('sets the timePicker to return data', async () => { + await PageObjects.timePicker.timePickerExists(); + + const fromTime = 'Jul 29, 2019 @ 00:00:00.000'; + const toTime = 'Jul 30, 2019 @ 00:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await retry.try(async () => { + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.not.contain('No services found'); + + expect(apmMainContainerTextItems).to.contain('opbeans-go'); + expect(apmMainContainerTextItems).to.contain('opbeans-node'); + expect(apmMainContainerTextItems).to.contain('opbeans-ruby'); + expect(apmMainContainerTextItems).to.contain('opbeans-python'); + expect(apmMainContainerTextItems).to.contain('opbeans-dotnet'); + expect(apmMainContainerTextItems).to.contain('opbeans-java'); + + expect(apmMainContainerTextItems).to.contain('development'); + + const items = await testSubjects.findAll('apmServiceListAppLink'); + expect(items.length).to.be(6); + }); + }); + + it(`navigates to the 'opbeans-go' service overview page`, async function () { + await find.clickByDisplayedLinkText(testData.serviceName); + + await retry.try(async () => { + const apmMainTemplateHeaderServiceName = await testSubjects.getVisibleTextAll( + 'apmMainTemplateHeaderServiceName' + ); + expect(apmMainTemplateHeaderServiceName).to.contain('opbeans-go'); + }); + }); + + it('shows the correlations flyout', async function () { + await testSubjects.click('apmViewCorrelationsButton'); + + await retry.try(async () => { + await testSubjects.existOrFail('apmCorrelationsFlyout', { + timeout: 10000, + }); + + const apmCorrelationsFlyoutHeader = await testSubjects.getVisibleText( + 'apmCorrelationsFlyoutHeader' + ); + + expect(apmCorrelationsFlyoutHeader).to.contain('Correlations BETA'); + }); + }); + + it('loads the correlation results', async function () { + await retry.try(async () => { + // Assert that the data fully loaded to 100% + const apmCorrelationsLatencyCorrelationsProgressTitle = await testSubjects.getVisibleText( + 'apmCorrelationsLatencyCorrelationsProgressTitle' + ); + expect(apmCorrelationsLatencyCorrelationsProgressTitle).to.be('Progress: 100%'); + + // Assert that the Correlations Chart and its header are present + const apmCorrelationsLatencyCorrelationsChartTitle = await testSubjects.getVisibleText( + 'apmCorrelationsLatencyCorrelationsChartTitle' + ); + expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be( + `Latency distribution for ${testData.serviceName}` + ); + await testSubjects.existOrFail('apmCorrelationsChart', { + timeout: 10000, + }); + + // Assert that results for the given service didn't find any correlations + const apmCorrelationsTable = await testSubjects.getVisibleText('apmCorrelationsTable'); + expect(apmCorrelationsTable).to.be('No significant correlations found'); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts index d2531b72e1b5..e4db5a66aa55 100644 --- a/x-pack/test/functional/apps/apm/index.ts +++ b/x-pack/test/functional/apps/apm/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('APM specs', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./correlations')); }); } diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index dc5afe4aa422..92cdc72ffc81 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); const log = getService('log'); - const pieChart = getService('pieChart'); + const elasticChart = getService('elasticChart'); const find = getService('find'); const renderable = getService('renderable'); const dashboardExpect = getService('dashboardExpect'); @@ -136,8 +136,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // check at least one visualization await renderable.waitForRender(); - log.debug('Checking pie charts rendered'); - await pieChart.expectPieSliceCount(4); + log.debug('Checking charts rendered'); + await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); await appMenu.clickLink('Discover'); await retry.try(async function () { @@ -147,8 +147,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await appMenu.clickLink('Dashboard'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - log.debug('Checking pie charts rendered'); - await pieChart.expectPieSliceCount(4); + log.debug('Checking charts rendered'); + await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); }); it('toggle from Discover to Dashboard attempt 1', async () => { @@ -160,8 +160,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await appMenu.clickLink('Dashboard'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - log.debug('Checking pie charts rendered'); - await pieChart.expectPieSliceCount(4); + log.debug('Checking charts rendered'); + await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); }); it('toggle from Discover to Dashboard attempt 2', async () => { @@ -173,11 +173,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await appMenu.clickLink('Dashboard'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - log.debug('Checking pie charts rendered'); - await pieChart.expectPieSliceCount(4); + log.debug('Checking charts rendered'); + await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); - log.debug('Checking area, bar and heatmap charts rendered'); - await dashboardExpect.seriesElementCount(15); log.debug('Checking saved searches rendered'); await dashboardExpect.savedSearchRowCount(10); log.debug('Checking input controls rendered'); @@ -185,8 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Checking tag cloud rendered'); await dashboardExpect.tagCloudWithValuesFound(['Sunny', 'Rain', 'Clear', 'Cloudy', 'Hail']); log.debug('Checking vega chart rendered'); - const tsvb = await find.existsByCssSelector('.vgaVis__view'); - expect(tsvb).to.be(true); + expect(await find.existsByCssSelector('.vgaVis__view')).to.be(true); }); }); } diff --git a/x-pack/test/functional/apps/maps/sample_data.js b/x-pack/test/functional/apps/maps/sample_data.js index 10e760fa9d94..fa12e5158ac7 100644 --- a/x-pack/test/functional/apps/maps/sample_data.js +++ b/x-pack/test/functional/apps/maps/sample_data.js @@ -135,7 +135,7 @@ export default function ({ getPageObjects, getService, updateBaselines }) { describe('flights', () => { before(async () => { - await PageObjects.maps.loadSavedMap('[Flights] Origin and Destination Flight Time'); + await PageObjects.maps.loadSavedMap('[Flights] Origin Time Delayed'); await PageObjects.maps.toggleLayerVisibility('Road map'); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json index a9837210c2e5..36a73c1994c9 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -8,7 +8,40 @@ "rule.id": "apm.error_rate", "message": "hello world 1", "kibana.rac.alert.owner": "apm", - "kibana.rac.alert.status": "open" + "kibana.rac.alert.status": "open", + "kibana.space_ids": ["space1", "space2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-observability-apm", + "id": "space1alert", + "source": { + "@timestamp": "2020-12-16T15:16:18.570Z", + "rule.id": "apm.error_rate", + "message": "hello world 1", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "kibana.space_ids": ["space1"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-observability-apm", + "id": "space2alert", + "source": { + "@timestamp": "2020-12-16T15:16:18.570Z", + "rule.id": "apm.error_rate", + "message": "hello world 1", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "kibana.space_ids": ["space2"] } } } @@ -23,7 +56,8 @@ "rule.id": "siem.signals", "message": "hello world security", "kibana.rac.alert.owner": "siem", - "kibana.rac.alert.status": "open" + "kibana.rac.alert.status": "open", + "kibana.space_ids": ["space1", "space2"] } } } diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts index cf3cc88f2cfc..05b55128fab3 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts @@ -98,6 +98,30 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); }); + it('superuser should be able to access an alert in a given space', async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=space1alert&index=${APM_ALERT_INDEX}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + it('superuser should NOT be able to access an alert in a space which the alert does not exist in', async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=space1alert&index=${APM_ALERT_INDEX}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it('obs only space 1 user should NOT be able to access an alert in a space which the user does not have access to', async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=space2alert&index=${APM_ALERT_INDEX}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .expect(404); + }); + function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) { authorizedUsers.forEach(({ username, password }) => { it(`${username} should be able to access alert ${alertId} in ${space}/${index}`, async () => { diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts index c126c434bd4c..14d903c7ac55 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts @@ -5,6 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; +import { omit } from 'lodash/fp'; import { superUser, @@ -104,14 +105,12 @@ export default ({ getService }: FtrProviderContext) => { _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), }) .expect(200); - expect(res.body).to.eql({ + expect(omit(['_version', '_seq_no'], res.body)).to.eql({ success: true, _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', result: 'updated', _shards: { total: 2, successful: 1, failed: 0 }, - _version: 'WzEsMV0=', - _seq_no: 1, _primary_term: 1, }); }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts index bd5f0896d96c..371b0f8d0b0b 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts @@ -24,12 +24,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); const dashboardPanelActions = getService('dashboardPanelActions'); const inspector = getService('inspector'); - const pieChart = getService('pieChart'); + const elasticChart = getService('elasticChart'); const find = getService('find'); const dashboardExpect = getService('dashboardExpect'); const searchSessions = getService('searchSessions'); - describe('save a search sessions with relative time', () => { + // Failing: See https://github.com/elastic/kibana/issues/97701 + describe.skip('save a search sessions with relative time', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, @@ -57,15 +58,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Saves and restores a session with relative time ranges', async () => { await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); await PageObjects.dashboard.waitForRenderComplete(); - await PageObjects.timePicker.pauseAutoRefresh(); // sample data has auto-refresh on await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.waitForRenderComplete(); - - // saving dashboard to populate map buffer. See https://github.com/elastic/kibana/pull/91148 for more info - // This can be removed after a fix to https://github.com/elastic/kibana/issues/98180 is completed - await PageObjects.dashboard.switchToEditMode(); - await PageObjects.dashboard.clickQuickSave(); - await PageObjects.dashboard.clickCancelOutOfEditMode(); await searchSessions.expectState('completed'); await searchSessions.save(); @@ -94,10 +87,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { async function checkSampleDashboardLoaded() { log.debug('Checking no error labels'); await testSubjects.missingOrFail('embeddableErrorLabel'); - log.debug('Checking pie charts rendered'); - await pieChart.expectPieSliceCount(4); - log.debug('Checking area, bar and heatmap charts rendered'); - await dashboardExpect.seriesElementCount(15); + log.debug('Checking charts rendered'); + await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); log.debug('Checking saved searches rendered'); await dashboardExpect.savedSearchRowCount(11); log.debug('Checking input controls rendered'); @@ -105,14 +96,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Checking tag cloud rendered'); await dashboardExpect.tagCloudWithValuesFound(['Sunny', 'Rain', 'Clear', 'Cloudy', 'Hail']); log.debug('Checking vega chart rendered'); - const tsvb = await find.existsByCssSelector('.vgaVis__view'); - expect(tsvb).to.be(true); + expect(await find.existsByCssSelector('.vgaVis__view')).to.be(true); log.debug('Checking map rendered'); - await dashboardPanelActions.openInspectorByTitle( - '[Flights] Origin and Destination Flight Time' - ); - await testSubjects.click('inspectorRequestChooser'); - await testSubjects.click(`inspectorRequestChooserFlight Origin Location`); + await dashboardPanelActions.openInspectorByTitle('[Flights] Origin Time Delayed'); const requestStats = await inspector.getTableData(); const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); expect(totalHits).to.equal('0'); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 1f57cd1b6db3..4656edc7edfb 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -23,7 +23,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + // Failing: See https://github.com/elastic/kibana/issues/106051 + describe.skip('test metadata api', () => { describe(`POST ${HOST_METADATA_LIST_ROUTE} when index is empty`, () => { it('metadata api should return empty result when index is empty', async () => { await deleteMetadataStream(getService); diff --git a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts index 17b457151bd9..0c43528ad8b8 100644 --- a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts @@ -128,7 +128,7 @@ export default function ({ }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.home.addSampleDataSet('flights'); - await PageObjects.maps.loadSavedMap('[Flights] Origin and Destination Flight Time'); + await PageObjects.maps.loadSavedMap('[Flights] Origin Time Delayed'); await PageObjects.maps.toggleLayerVisibility('Road map'); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen();