diff --git a/docs/siem/images/detections-ui.png b/docs/siem/images/detections-ui.png new file mode 100644 index 0000000000000..3139ffea0767d Binary files /dev/null and b/docs/siem/images/detections-ui.png differ diff --git a/docs/siem/images/hosts-ui.png b/docs/siem/images/hosts-ui.png index 2569df8f419b8..be9fd29246b51 100644 Binary files a/docs/siem/images/hosts-ui.png and b/docs/siem/images/hosts-ui.png differ diff --git a/docs/siem/images/network-ui.png b/docs/siem/images/network-ui.png index 406ac854cd0a2..de8ce89273a02 100644 Binary files a/docs/siem/images/network-ui.png and b/docs/siem/images/network-ui.png differ diff --git a/docs/siem/images/overview-ui.png b/docs/siem/images/overview-ui.png index a34b2fea061c9..6ac02104d6123 100644 Binary files a/docs/siem/images/overview-ui.png and b/docs/siem/images/overview-ui.png differ diff --git a/docs/siem/siem-ui.asciidoc b/docs/siem/siem-ui.asciidoc index 7294cfbc414f4..f01575a21b9f6 100644 --- a/docs/siem/siem-ui.asciidoc +++ b/docs/siem/siem-ui.asciidoc @@ -33,6 +33,23 @@ investigation. [role="screenshot"] image::siem/images/network-ui.png[] +[float] +[[detections-ui]] +=== Detections + +The Detections feature automatically searches for threats and creates +signals when they are detected. Signal detection rules define the conditions +for creating signals. The SIEM app comes with prebuilt rules that search for +suspicious activity on your network and hosts. Additionally, you can +create your own rules. + +See {siem-guide}/detection-engine-overview.html[Detections] in the SIEM +Guide for information on managing detection rules and signals via the UI +or the Detections API. + +[role="screenshot"] +image::siem/images/detections-ui.png[] + [float] [[timelines-ui]] === Timeline diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts index 769347a26c34c..ba7faf8c34b59 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_config.ts @@ -58,6 +58,11 @@ const unknownSchema: Schema = { defaults: {}, editor: false, group: AggGroupNames.Metrics, + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, }; const getTypeFromRegistry = (type: string): IAggType => { @@ -438,7 +443,7 @@ export class AggConfig { if (fieldParam) { // @ts-ignore - availableFields = fieldParam.getAvailableFields(this.getIndexPattern().fields); + availableFields = fieldParam.getAvailableFields(this); } // clear out the previous params except for a few special ones diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts index e7d286c187ef8..3bae7b92618dc 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/metric_agg_type.ts @@ -24,6 +24,7 @@ import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../agg_config'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +import { FilterFieldTypes } from '../param_types/field'; export interface IMetricAggConfig extends AggConfig { type: InstanceType; @@ -31,7 +32,7 @@ export interface IMetricAggConfig extends AggConfig { export interface MetricAggParam extends AggParamType { - filterFieldTypes?: KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; + filterFieldTypes?: FilterFieldTypes; onlyAggregatable?: boolean; } diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts index 81bd14ded75b0..3112d882bb87e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/top_hit.ts @@ -20,7 +20,6 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; -import { aggTypeFieldFilters } from '../param_types/filter'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; @@ -33,17 +32,6 @@ const isNumericFieldSelected = (agg: IMetricAggConfig) => { return field && field.type && field.type === KBN_FIELD_TYPES.NUMBER; }; -aggTypeFieldFilters.addFilter((field, aggConfig) => { - if ( - aggConfig.type.name !== METRIC_TYPES.TOP_HITS || - _.get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) - ) { - return true; - } - - return field.type === KBN_FIELD_TYPES.NUMBER; -}); - export const topHitMetricAgg = new MetricAggType({ name: METRIC_TYPES.TOP_HITS, title: i18n.translate('data.search.aggs.metrics.topHitTitle', { @@ -75,7 +63,10 @@ export const topHitMetricAgg = new MetricAggType({ name: 'field', type: 'field', onlyAggregatable: false, - filterFieldTypes: '*', + filterFieldTypes: (aggConfig: IMetricAggConfig) => + _.get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) + ? '*' + : KBN_FIELD_TYPES.NUMBER, write(agg, output) { const field = agg.getParam('field'); output.params = {}; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts index d0fa711d89c70..fa88754ac60b9 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.test.ts @@ -17,9 +17,13 @@ * under the License. */ +import { get } from 'lodash'; import { BaseParamType } from './base'; import { FieldParamType } from './field'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +import { IAggConfig } from '../agg_config'; +import { IMetricAggConfig } from '../metrics/metric_agg_type'; +import { Schema } from '../schemas'; jest.mock('ui/new_platform'); @@ -45,7 +49,11 @@ describe('Field', () => { searchable: true, }, ], - } as any; + }; + + const agg = ({ + getIndexPattern: jest.fn(() => indexPattern), + } as unknown) as IAggConfig; describe('constructor', () => { it('it is an instance of BaseParamType', () => { @@ -65,7 +73,7 @@ describe('Field', () => { type: 'field', }); - const fields = aggParam.getAvailableFields(indexPattern.fields); + const fields = aggParam.getAvailableFields(agg); expect(fields.length).toBe(1); @@ -82,7 +90,58 @@ describe('Field', () => { aggParam.onlyAggregatable = false; - const fields = aggParam.getAvailableFields(indexPattern.fields); + const fields = aggParam.getAvailableFields(agg); + + expect(fields.length).toBe(2); + }); + + it('should return all fields if filterFieldTypes was not specified', () => { + const aggParam = new FieldParamType({ + name: 'field', + type: 'field', + }); + + indexPattern.fields[1].aggregatable = true; + + const fields = aggParam.getAvailableFields(agg); + + expect(fields.length).toBe(2); + }); + + it('should return only numeric fields if filterFieldTypes was specified as a function', () => { + const aggParam = new FieldParamType({ + name: 'field', + type: 'field', + filterFieldTypes: (aggConfig: IMetricAggConfig) => + get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) + ? '*' + : KBN_FIELD_TYPES.NUMBER, + }); + const fields = aggParam.getAvailableFields(agg); + + expect(fields.length).toBe(1); + expect(fields[0].type).toBe(KBN_FIELD_TYPES.NUMBER); + }); + + it('should return all fields if filterFieldTypes was specified as a function and aggSettings allow string type fields', () => { + const aggParam = new FieldParamType({ + name: 'field', + type: 'field', + filterFieldTypes: (aggConfig: IMetricAggConfig) => + get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) + ? '*' + : KBN_FIELD_TYPES.NUMBER, + }); + + agg.schema = { + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + } as Schema; + + const fields = aggParam.getAvailableFields(agg); expect(fields.length).toBe(2); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts index c41c159ad0f78..9a204bb151e2d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts @@ -17,24 +17,27 @@ * under the License. */ -// @ts-ignore import { i18n } from '@kbn/i18n'; +import { isFunction } from 'lodash'; import { npStart } from 'ui/new_platform'; -import { AggConfig } from '../agg_config'; +import { IAggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; import { propFilter } from '../filter'; -import { Field, IFieldList, isNestedField } from '../../../../../../../plugins/data/public'; +import { IMetricAggConfig } from '../metrics/metric_agg_type'; +import { Field, isNestedField, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; const filterByType = propFilter('type'); +type FieldTypes = KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; +export type FilterFieldTypes = ((aggConfig: IMetricAggConfig) => FieldTypes) | FieldTypes; // TODO need to make a more explicit interface for this export type IFieldParamType = FieldParamType; export class FieldParamType extends BaseParamType { required = true; scriptable = true; - filterFieldTypes: string; + filterFieldTypes: FilterFieldTypes; onlyAggregatable: boolean; constructor(config: Record) { @@ -44,7 +47,7 @@ export class FieldParamType extends BaseParamType { this.onlyAggregatable = config.onlyAggregatable !== false; if (!config.write) { - this.write = (aggConfig: AggConfig, output: Record) => { + this.write = (aggConfig: IAggConfig, output: Record) => { const field = aggConfig.getField(); if (!field) { @@ -73,7 +76,7 @@ export class FieldParamType extends BaseParamType { return field.name; }; - this.deserialize = (fieldName: string, aggConfig?: AggConfig) => { + this.deserialize = (fieldName: string, aggConfig?: IAggConfig) => { if (!aggConfig) { throw new Error('aggConfig was not provided to FieldParamType deserialize function'); } @@ -84,9 +87,7 @@ export class FieldParamType extends BaseParamType { } // @ts-ignore - const validField = this.getAvailableFields(aggConfig.getIndexPattern().fields).find( - (f: any) => f.name === fieldName - ); + const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName); if (!validField) { npStart.core.notifications.toasts.addDanger( i18n.translate( @@ -109,7 +110,8 @@ export class FieldParamType extends BaseParamType { /** * filter the fields to the available ones */ - getAvailableFields = (fields: IFieldList) => { + getAvailableFields = (aggConfig: IAggConfig) => { + const fields = aggConfig.getIndexPattern().fields; const filteredFields = fields.filter((field: Field) => { const { onlyAggregatable, scriptable, filterFieldTypes } = this; @@ -120,8 +122,10 @@ export class FieldParamType extends BaseParamType { return false; } - if (!filterFieldTypes) { - return true; + if (isFunction(filterFieldTypes)) { + const filter = filterFieldTypes(aggConfig as IMetricAggConfig); + + return filterByType([field], filter).length !== 0; } return filterByType([field], filterFieldTypes).length !== 0; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8c35044b52c9e..395e0da218307 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -97,13 +97,8 @@ export default function(kibana) { }), order: -1001, url: `${kbnBaseUrl}#/dashboards`, - // The subUrlBase is the common substring of all urls for this app. If not given, it defaults to the url - // above. This app has to use a different subUrlBase, in addition to the url above, because "#/dashboard" - // routes to a page that creates a new dashboard. When we introduced a landing page, we needed to change - // the url above in order to preserve the original url for BWC. The subUrlBase helps the Chrome api nav - // to determine what url to use for the app link. - subUrlBase: `${kbnBaseUrl}#/dashboard`, euiIconType: 'dashboardApp', + disableSubUrlTracking: true, category: DEFAULT_APP_CATEGORIES.analyze, }, { diff --git a/src/legacy/core_plugins/kibana/public/.eslintrc.js b/src/legacy/core_plugins/kibana/public/.eslintrc.js index 9b45217287dc8..b3ee0a8fa7b04 100644 --- a/src/legacy/core_plugins/kibana/public/.eslintrc.js +++ b/src/legacy/core_plugins/kibana/public/.eslintrc.js @@ -17,8 +17,15 @@ * under the License. */ +const topLevelConfig = require('../../../../../.eslintrc.js'); const path = require('path'); +const topLevelRestricedZones = topLevelConfig.overrides.find( + override => + override.files[0] === '**/*.{js,ts,tsx}' && + Object.keys(override.rules)[0] === '@kbn/eslint/no-restricted-paths' +).rules['@kbn/eslint/no-restricted-paths'][1].zones; + /** * Builds custom restricted paths configuration for the shimmed plugins within the kibana plugin. * These custom rules extend the default checks in the top level `eslintrc.js` by also checking two other things: @@ -28,34 +35,37 @@ const path = require('path'); * @returns zones configuration for the no-restricted-paths linter */ function buildRestrictedPaths(shimmedPlugins) { - return shimmedPlugins.map(shimmedPlugin => ([{ - target: [ - `src/legacy/core_plugins/kibana/public/${shimmedPlugin}/np_ready/**/*`, - ], - from: [ - 'ui/**/*', - 'src/legacy/ui/**/*', - 'src/legacy/core_plugins/kibana/public/**/*', - 'src/legacy/core_plugins/data/public/**/*', - '!src/legacy/core_plugins/data/public/index.ts', - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, - ], - allowSameFolder: false, - errorMessage: `${shimmedPlugin} is a shimmed plugin that is not allowed to import modules from the legacy platform. If you need legacy modules for the transition period, import them either in the legacy_imports, kibana_services or index module.`, - }, { - target: [ - 'src/**/*', - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, - 'x-pack/**/*', - ], - from: [ - `src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`, - `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`, - ], - allowSameFolder: false, - errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`, - }])).reduce((acc, part) => [...acc, ...part], []); + return shimmedPlugins + .map(shimmedPlugin => [ + { + target: [`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/np_ready/**/*`], + from: [ + 'ui/**/*', + 'src/legacy/ui/**/*', + 'src/legacy/core_plugins/kibana/public/**/*', + 'src/legacy/core_plugins/data/public/**/*', + '!src/legacy/core_plugins/data/public/index.ts', + `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, + ], + allowSameFolder: false, + errorMessage: `${shimmedPlugin} is a shimmed plugin that is not allowed to import modules from the legacy platform. If you need legacy modules for the transition period, import them either in the legacy_imports, kibana_services or index module.`, + }, + { + target: [ + 'src/**/*', + `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, + 'x-pack/**/*', + ], + from: [ + `src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, + `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`, + `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`, + ], + allowSameFolder: false, + errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`, + }, + ]) + .reduce((acc, part) => [...acc, ...part], []); } module.exports = { @@ -66,7 +76,9 @@ module.exports = { 'error', { basePath: path.resolve(__dirname, '../../../../../'), - zones: buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools', 'home']), + zones: topLevelRestricedZones.concat( + buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools', 'home']) + ), }, ], }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts index acbc4c4b6c47f..ca2dc9d5fb4f5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts @@ -41,6 +41,7 @@ async function getAngularDependencies(): Promise(() => ({})); + private stopUrlTracking: (() => void) | undefined = undefined; + public setup( core: CoreSetup, - { __LEGACY: { getAngularDependencies }, home, kibana_legacy }: DashboardPluginSetupDependencies + { + __LEGACY: { getAngularDependencies }, + home, + kibana_legacy, + npData, + }: DashboardPluginSetupDependencies ) { + const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( + npData.query + ); + const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/kibana'), + defaultSubUrl: '#/dashboards', + storageKey: 'lastUrl:dashboard', + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + kbnUrlKey: '_g', + stateUpdate$: querySyncStateContainer.state$, + }, + ], + }); + this.stopUrlTracking = () => { + stopQuerySyncStateContainer(); + stopUrlTracker(); + }; const app: App = { id: '', title: 'Dashboards', @@ -81,6 +119,7 @@ export class DashboardPlugin implements Plugin { if (this.startDependencies === null) { throw new Error('not started yet'); } + appMounted(); const { savedObjectsClient, embeddables, @@ -114,10 +153,20 @@ export class DashboardPlugin implements Plugin { localStorage: new Storage(localStorage), }; const { renderApp } = await import('./np_ready/application'); - return renderApp(params.element, params.appBasePath, deps); + const unmount = renderApp(params.element, params.appBasePath, deps); + return () => { + unmount(); + appUnMounted(); + }; }, }; - kibana_legacy.registerLegacyApp({ ...app, id: 'dashboard' }); + kibana_legacy.registerLegacyApp({ + ...app, + id: 'dashboard', + // only register the updater in once app, otherwise all updates would happen twice + updater$: this.appStateUpdater.asObservable(), + navLinkId: 'kibana:dashboard', + }); kibana_legacy.registerLegacyApp({ ...app, id: 'dashboards' }); home.featureCatalogue.register({ @@ -147,4 +196,10 @@ export class DashboardPlugin implements Plugin { share, }; } + + stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } } diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/instruction_set.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/instruction_set.js index 15bda33534185..631ef1d6e0e42 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/instruction_set.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/instruction_set.js @@ -22,7 +22,7 @@ import PropTypes from 'prop-types'; import { Instruction } from './instruction'; import { ParameterForm } from './parameter_form'; import { Content } from './content'; -import { getDisplayText } from '../../../../../../../../plugins/home/server/tutorials/instructions/instruction_variant'; +import { getDisplayText } from '../../../../../../../../plugins/home/public'; import { EuiTabs, EuiTab, diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index d52bec8304ff9..c84a3e1eacbd2 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -79,6 +79,17 @@ export class LocalApplicationService { })(); }, }); + + if (app.updater$) { + app.updater$.subscribe(updater => { + const updatedFields = updater(app); + if (updatedFields && updatedFields.activeUrl) { + npStart.core.chrome.navLinks.update(app.navLinkId || app.id, { + url: updatedFields.activeUrl, + }); + } + }); + } }); npStart.plugins.kibana_legacy.getForwards().forEach(({ legacyAppId, newAppId, keepPrefix }) => { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js index c990efaf43547..e10f033ed8165 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js @@ -279,13 +279,12 @@ export class StepIndexPattern extends Component { render() { const { isIncludingSystemIndices, allIndices } = this.props; - const { query, partialMatchedIndices, exactMatchedIndices } = this.state; + const { partialMatchedIndices, exactMatchedIndices } = this.state; const matchedIndices = getMatchedIndices( allIndices, partialMatchedIndices, exactMatchedIndices, - query, isIncludingSystemIndices ); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/constants/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/constants/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/constants/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/constants/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.error.json b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.error.json deleted file mode 100644 index acb9a9ecd0206..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "statusCode": 400, - "error": "Bad Request" -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.exception.json b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.exception.json deleted file mode 100644 index 1406b06813637..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.exception.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "body": { - "error": { - "root_cause": [ - { - "type": "index_not_found_exception", - "reason": "no such index", - "index_uuid": "_na_", - "resource.type": "index_or_alias", - "resource.id": "t", - "index": "t" - } - ], - "type": "transport_exception", - "reason": "unable to communicate with remote cluster [cluster_one]", - "caused_by": { - "type": "index_not_found_exception", - "reason": "no such index", - "index_uuid": "_na_", - "resource.type": "index_or_alias", - "resource.id": "t", - "index": "t" - } - } - }, - "status": 500 -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.success.json b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.success.json deleted file mode 100644 index 1b261243ca728..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/api/get_indices.success.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "hits": { - "total": 1, - "max_score": 0.0, - "hits": [] - }, - "aggregations": { - "indices": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [{ - "key": "1", - "doc_count": 1 - },{ - "key": "2", - "doc_count": 1 - }] - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/get_indices.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/get_indices.test.js deleted file mode 100644 index 924b0dc46d74d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/get_indices.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { getIndices } from '../get_indices'; -import successfulResponse from './api/get_indices.success.json'; -import errorResponse from './api/get_indices.error.json'; -import exceptionResponse from './api/get_indices.exception.json'; -const mockIndexPatternCreationType = { - getIndexPatternType: () => 'default', - getIndexPatternName: () => 'name', - checkIndicesForErrors: () => false, - getShowSystemIndices: () => false, - renderPrompt: () => {}, - getIndexPatternMappings: () => { - return {}; - }, - getIndexTags: () => { - return []; - }, -}; - -describe('getIndices', () => { - it('should work in a basic case', async () => { - const es = { - search: () => new Promise(resolve => resolve(successfulResponse)), - }; - - const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1); - expect(result.length).toBe(2); - expect(result[0].name).toBe('1'); - expect(result[1].name).toBe('2'); - }); - - it('should ignore ccs query-all', async () => { - expect((await getIndices(null, mockIndexPatternCreationType, '*:')).length).toBe(0); - }); - - it('should ignore a single comma', async () => { - expect((await getIndices(null, mockIndexPatternCreationType, ',')).length).toBe(0); - expect((await getIndices(null, mockIndexPatternCreationType, ',*')).length).toBe(0); - expect((await getIndices(null, mockIndexPatternCreationType, ',foobar')).length).toBe(0); - }); - - it('should trim the input', async () => { - let index; - const es = { - search: jest.fn().mockImplementation(params => { - index = params.index; - }), - }; - - await getIndices(es, mockIndexPatternCreationType, 'kibana ', 1); - expect(index).toBe('kibana'); - }); - - it('should use the limit', async () => { - let limit; - const es = { - search: jest.fn().mockImplementation(params => { - limit = params.body.aggs.indices.terms.size; - }), - }; - - await getIndices(es, mockIndexPatternCreationType, 'kibana', 10); - expect(limit).toBe(10); - }); - - describe('errors', () => { - it('should handle errors gracefully', async () => { - const es = { - search: () => new Promise(resolve => resolve(errorResponse)), - }; - - const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1); - expect(result.length).toBe(0); - }); - - it('should throw exceptions', async () => { - const es = { - search: () => { - throw new Error('Fail'); - }, - }; - - await expect(getIndices(es, mockIndexPatternCreationType, 'kibana', 1)).rejects.toThrow(); - }); - - it('should handle index_not_found_exception errors gracefully', async () => { - const es = { - search: () => new Promise((resolve, reject) => reject(exceptionResponse)), - }; - - const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1); - expect(result.length).toBe(0); - }); - - it('should throw an exception if no limit is provided', async () => { - await expect(getIndices({}, mockIndexPatternCreationType, 'kibana')).rejects.toThrow(); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/can_append_wildcard.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.test.ts similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/can_append_wildcard.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.test.ts index 055632bdd19e0..14139c2e08dc0 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/can_append_wildcard.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.test.ts @@ -17,13 +17,9 @@ * under the License. */ -import { canAppendWildcard } from '../can_append_wildcard'; +import { canAppendWildcard } from './can_append_wildcard'; describe('canAppendWildcard', () => { - test('ignores no data', () => { - expect(canAppendWildcard({})).toBeFalsy(); - }); - test('ignores symbols', () => { expect(canAppendWildcard('%')).toBeFalsy(); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.ts similarity index 94% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.ts index b47e645730aef..e9c4f75e4313b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/can_append_wildcard.ts @@ -17,7 +17,7 @@ * under the License. */ -export const canAppendWildcard = keyPressed => { +export const canAppendWildcard = (keyPressed: string) => { // If it's not a letter, number or is something longer, reject it if (!keyPressed || !/[a-z0-9]/i.test(keyPressed) || keyPressed.length !== 1) { return false; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_illegal_characters.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_illegal_characters.ts similarity index 90% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_illegal_characters.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_illegal_characters.ts index 31485bb3daaa2..ca4fc8122903c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_illegal_characters.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_illegal_characters.ts @@ -17,6 +17,6 @@ * under the License. */ -export function containsIllegalCharacters(pattern, illegalCharacters) { +export function containsIllegalCharacters(pattern: string, illegalCharacters: string[]) { return illegalCharacters.some(char => pattern.includes(char)); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/contains_invalid_characters.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_invalid_characters.test.ts similarity index 93% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/contains_invalid_characters.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_invalid_characters.test.ts index 05c4aba2571bd..640908d3db6d1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/contains_invalid_characters.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/contains_invalid_characters.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { containsIllegalCharacters } from '../contains_illegal_characters'; +import { containsIllegalCharacters } from './contains_illegal_characters'; describe('containsIllegalCharacters', () => { it('returns true with illegal characters', () => { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/ensure_minimum_time.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.test.ts similarity index 96% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/ensure_minimum_time.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.test.ts index 99724cbf3a2a7..e5fcfe056923a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/ensure_minimum_time.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ensureMinimumTime } from '../ensure_minimum_time'; +import { ensureMinimumTime } from './ensure_minimum_time'; describe('ensureMinimumTime', () => { it('resolves single promise', async done => { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.ts index 0a6d3fcfbbdf0..84852ece485eb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/ensure_minimum_time.ts @@ -27,7 +27,7 @@ export const DEFAULT_MINIMUM_TIME_MS = 300; export async function ensureMinimumTime( - promiseOrPromises, + promiseOrPromises: Promise | Array>, minimumTimeMs = DEFAULT_MINIMUM_TIME_MS ) { let returnValue; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/extract_time_fields.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.test.ts similarity index 89% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/extract_time_fields.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.test.ts index ec420e19817c7..4cd28090420a7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/extract_time_fields.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.test.ts @@ -17,11 +17,14 @@ * under the License. */ -import { extractTimeFields } from '../extract_time_fields'; +import { extractTimeFields } from './extract_time_fields'; describe('extractTimeFields', () => { it('should handle no date fields', () => { - const fields = [{ type: 'text' }, { type: 'text' }]; + const fields = [ + { type: 'text', name: 'name' }, + { type: 'text', name: 'name' }, + ]; expect(extractTimeFields(fields)).toEqual([ { display: `The indices which match this index pattern don't contain any time fields.` }, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.ts similarity index 92% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.ts index 1a9deefb217f2..0b95ec0a120da 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/extract_time_fields.ts @@ -18,8 +18,9 @@ */ import { i18n } from '@kbn/i18n'; +import { IFieldType } from '../../../../../../../../../plugins/data/public'; -export function extractTimeFields(fields) { +export function extractTimeFields(fields: IFieldType[]) { const dateFields = fields.filter(field => field.type === 'date'); const label = i18n.translate('kbn.management.createIndexPattern.stepTime.noTimeFieldsLabel', { defaultMessage: "The indices which match this index pattern don't contain any time fields.", diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts new file mode 100644 index 0000000000000..cd7c8278adcc7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts @@ -0,0 +1,167 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getIndices } from './get_indices'; +import { IndexPatternCreationConfig } from './../../../../../../../management/public'; +import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public'; + +export const successfulResponse = { + hits: { + total: 1, + max_score: 0.0, + hits: [], + }, + aggregations: { + indices: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '1', + doc_count: 1, + }, + { + key: '2', + doc_count: 1, + }, + ], + }, + }, +}; + +export const exceptionResponse = { + body: { + error: { + root_cause: [ + { + type: 'index_not_found_exception', + reason: 'no such index', + index_uuid: '_na_', + 'resource.type': 'index_or_alias', + 'resource.id': 't', + index: 't', + }, + ], + type: 'transport_exception', + reason: 'unable to communicate with remote cluster [cluster_one]', + caused_by: { + type: 'index_not_found_exception', + reason: 'no such index', + index_uuid: '_na_', + 'resource.type': 'index_or_alias', + 'resource.id': 't', + index: 't', + }, + }, + }, + status: 500, +}; + +export const errorResponse = { + statusCode: 400, + error: 'Bad Request', +}; + +const mockIndexPatternCreationType = new IndexPatternCreationConfig({ + type: 'default', + name: 'name', + showSystemIndices: false, + httpClient: {}, + isBeta: false, +}); + +function esClientFactory(search: (params: any) => any): LegacyApiCaller { + return { + search, + msearch: () => ({ + abort: () => {}, + ...new Promise(resolve => resolve({})), + }), + }; +} + +const es = esClientFactory(() => successfulResponse); + +describe('getIndices', () => { + it('should work in a basic case', async () => { + const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1); + expect(result.length).toBe(2); + expect(result[0].name).toBe('1'); + expect(result[1].name).toBe('2'); + }); + + it('should ignore ccs query-all', async () => { + expect((await getIndices(es, mockIndexPatternCreationType, '*:', 10)).length).toBe(0); + }); + + it('should ignore a single comma', async () => { + expect((await getIndices(es, mockIndexPatternCreationType, ',', 10)).length).toBe(0); + expect((await getIndices(es, mockIndexPatternCreationType, ',*', 10)).length).toBe(0); + expect((await getIndices(es, mockIndexPatternCreationType, ',foobar', 10)).length).toBe(0); + }); + + it('should trim the input', async () => { + let index; + const esClient = esClientFactory( + jest.fn().mockImplementation(params => { + index = params.index; + }) + ); + + await getIndices(esClient, mockIndexPatternCreationType, 'kibana ', 1); + expect(index).toBe('kibana'); + }); + + it('should use the limit', async () => { + let limit; + const esClient = esClientFactory( + jest.fn().mockImplementation(params => { + limit = params.body.aggs.indices.terms.size; + }) + ); + await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 10); + expect(limit).toBe(10); + }); + + describe('errors', () => { + it('should handle errors gracefully', async () => { + const esClient = esClientFactory(() => errorResponse); + const result = await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1); + expect(result.length).toBe(0); + }); + + it('should throw exceptions', async () => { + const esClient = esClientFactory(() => { + throw new Error('Fail'); + }); + + await expect( + getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1) + ).rejects.toThrow(); + }); + + it('should handle index_not_found_exception errors gracefully', async () => { + const esClient = esClientFactory( + () => new Promise((resolve, reject) => reject(exceptionResponse)) + ); + const result = await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1); + expect(result.length).toBe(0); + }); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts similarity index 83% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts index 8159fff8220bd..3848c425e2d49 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.ts @@ -18,8 +18,16 @@ */ import { get, sortBy } from 'lodash'; +import { IndexPatternCreationConfig } from '../../../../../../../management/public'; +import { DataPublicPluginStart } from '../../../../../../../../../plugins/data/public'; +import { MatchedIndex } from '../types'; -export async function getIndices(es, indexPatternCreationType, rawPattern, limit) { +export async function getIndices( + es: DataPublicPluginStart['search']['__LEGACY']['esClient'], + indexPatternCreationType: IndexPatternCreationConfig, + rawPattern: string, + limit: number +): Promise { const pattern = rawPattern.trim(); // Searching for `*:` fails for CCS environments. The search request @@ -70,10 +78,10 @@ export async function getIndices(es, indexPatternCreationType, rawPattern, limit return sortBy( response.aggregations.indices.buckets - .map(bucket => { + .map((bucket: { key: string; doc_count: number }) => { return bucket.key; }) - .map(indexName => { + .map((indexName: string) => { return { name: indexName, tags: indexPatternCreationType.getIndexTags(indexName), diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/get_matched_indices.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.test.ts similarity index 53% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/get_matched_indices.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.test.ts index 625c128181ffe..7aba50a7ca12b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/__jest__/get_matched_indices.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.test.ts @@ -17,24 +17,32 @@ * under the License. */ -import { getMatchedIndices } from '../get_matched_indices'; +import { getMatchedIndices } from './get_matched_indices'; -jest.mock('../../constants', () => ({ +jest.mock('./../constants', () => ({ MAX_NUMBER_OF_MATCHING_INDICES: 6, })); +const tags: string[] = []; const indices = [ - { name: 'kibana' }, - { name: 'es' }, - { name: 'logstash' }, - { name: 'packetbeat' }, - { name: 'metricbeat' }, - { name: '.kibana' }, + { name: 'kibana', tags }, + { name: 'es', tags }, + { name: 'logstash', tags }, + { name: 'packetbeat', tags }, + { name: 'metricbeat', tags }, + { name: '.kibana', tags }, ]; -const partialIndices = [{ name: 'kibana' }, { name: 'es' }, { name: '.kibana' }]; +const partialIndices = [ + { name: 'kibana', tags }, + { name: 'es', tags }, + { name: '.kibana', tags }, +]; -const exactIndices = [{ name: 'kibana' }, { name: '.kibana' }]; +const exactIndices = [ + { name: 'kibana', tags }, + { name: '.kibana', tags }, +]; describe('getMatchedIndices', () => { it('should return all indices', () => { @@ -43,26 +51,32 @@ describe('getMatchedIndices', () => { exactMatchedIndices, partialMatchedIndices, visibleIndices, - } = getMatchedIndices(indices, partialIndices, exactIndices, '*', true); + } = getMatchedIndices(indices, partialIndices, exactIndices, true); expect(allIndices).toEqual([ - { name: 'kibana' }, - { name: 'es' }, - { name: 'logstash' }, - { name: 'packetbeat' }, - { name: 'metricbeat' }, - { name: '.kibana' }, + { name: 'kibana', tags }, + { name: 'es', tags }, + { name: 'logstash', tags }, + { name: 'packetbeat', tags }, + { name: 'metricbeat', tags }, + { name: '.kibana', tags }, ]); - expect(exactMatchedIndices).toEqual([{ name: 'kibana' }, { name: '.kibana' }]); + expect(exactMatchedIndices).toEqual([ + { name: 'kibana', tags }, + { name: '.kibana', tags }, + ]); expect(partialMatchedIndices).toEqual([ - { name: 'kibana' }, - { name: 'es' }, - { name: '.kibana' }, + { name: 'kibana', tags }, + { name: 'es', tags }, + { name: '.kibana', tags }, ]); - expect(visibleIndices).toEqual([{ name: 'kibana' }, { name: '.kibana' }]); + expect(visibleIndices).toEqual([ + { name: 'kibana', tags }, + { name: '.kibana', tags }, + ]); }); it('should return all indices except for system indices', () => { @@ -71,31 +85,38 @@ describe('getMatchedIndices', () => { exactMatchedIndices, partialMatchedIndices, visibleIndices, - } = getMatchedIndices(indices, partialIndices, exactIndices, '*', false); + } = getMatchedIndices(indices, partialIndices, exactIndices, false); expect(allIndices).toEqual([ - { name: 'kibana' }, - { name: 'es' }, - { name: 'logstash' }, - { name: 'packetbeat' }, - { name: 'metricbeat' }, + { name: 'kibana', tags }, + { name: 'es', tags }, + { name: 'logstash', tags }, + { name: 'packetbeat', tags }, + { name: 'metricbeat', tags }, ]); - expect(exactMatchedIndices).toEqual([{ name: 'kibana' }]); + expect(exactMatchedIndices).toEqual([{ name: 'kibana', tags }]); - expect(partialMatchedIndices).toEqual([{ name: 'kibana' }, { name: 'es' }]); + expect(partialMatchedIndices).toEqual([ + { name: 'kibana', tags }, + { name: 'es', tags }, + ]); - expect(visibleIndices).toEqual([{ name: 'kibana' }]); + expect(visibleIndices).toEqual([{ name: 'kibana', tags }]); }); it('should return partial matches as visible if there are no exact', () => { - const { visibleIndices } = getMatchedIndices(indices, partialIndices, [], '*', true); + const { visibleIndices } = getMatchedIndices(indices, partialIndices, [], true); - expect(visibleIndices).toEqual([{ name: 'kibana' }, { name: 'es' }, { name: '.kibana' }]); + expect(visibleIndices).toEqual([ + { name: 'kibana', tags }, + { name: 'es', tags }, + { name: '.kibana', tags }, + ]); }); it('should return all indices as visible if there are no exact or partial', () => { - const { visibleIndices } = getMatchedIndices(indices, [], [], '*', true); + const { visibleIndices } = getMatchedIndices(indices, [], [], true); expect(visibleIndices).toEqual(indices); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.ts similarity index 86% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.ts index 19a829a83a2b2..cc3fd4075aa0e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_matched_indices.ts @@ -19,19 +19,21 @@ import { MAX_NUMBER_OF_MATCHING_INDICES } from '../constants'; -function isSystemIndex(index) { +function isSystemIndex(index: string): boolean { if (index.startsWith('.')) { return true; } if (index.includes(':')) { - return index.split(':').reduce((isSystem, index) => isSystem || isSystemIndex(index), false); + return index + .split(':') + .reduce((isSystem: boolean, idx) => isSystem || isSystemIndex(idx), false); } return false; } -function filterSystemIndices(indices, isIncludingSystemIndices) { +function filterSystemIndices(indices: MatchedIndex[], isIncludingSystemIndices: boolean) { if (!indices) { return indices; } @@ -62,12 +64,14 @@ function filterSystemIndices(indices, isIncludingSystemIndices) { This is the result of searching against a query that already ends in `*`. We call this `exact` matches because ES is telling us exactly what it matches */ + +import { MatchedIndex } from '../types'; + export function getMatchedIndices( - unfilteredAllIndices, - unfilteredPartialMatchedIndices, - unfilteredExactMatchedIndices, - query, - isIncludingSystemIndices + unfilteredAllIndices: MatchedIndex[], + unfilteredPartialMatchedIndices: MatchedIndex[], + unfilteredExactMatchedIndices: MatchedIndex[], + isIncludingSystemIndices: boolean = false ) { const allIndices = filterSystemIndices(unfilteredAllIndices, isIncludingSystemIndices); const partialMatchedIndices = filterSystemIndices( diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/index.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/index.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/types.ts new file mode 100644 index 0000000000000..93bb6920c6981 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/types.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface MatchedIndex { + name: string; + tags: string[]; +} diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts index 0598c88c80ba7..b68b2e40aad9e 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts +++ b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/config.ts @@ -102,7 +102,7 @@ export class IndexPatternCreationConfig { return this.showSystemIndices; } - public getIndexTags() { + public getIndexTags(indexName: string) { return []; } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts index b9fb81fccd32c..f3bee80baa1ba 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IndexPattern, Field } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggConfig, IAggType, AggGroupNames, BUCKET_TYPES, IndexedArray } from '../legacy_imports'; +import { IAggConfig, IAggType, AggGroupNames, BUCKET_TYPES } from '../legacy_imports'; import { getAggParamsToRender, getAggTypeOptions, @@ -105,8 +105,10 @@ describe('DefaultEditorAggParams helpers', () => { name: 'field', type: 'field', filterFieldTypes, - getAvailableFields: jest.fn((fields: IndexedArray) => - fields.filter(({ type }) => filterFieldTypes.includes(type)) + getAvailableFields: jest.fn((aggConfig: IAggConfig) => + aggConfig + .getIndexPattern() + .fields.filter(({ type }) => filterFieldTypes.includes(type)) ), }, { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts index 25aa21fc83b31..124c41a50c0df 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -73,9 +73,7 @@ function getAggParamsToRender({ agg, editorConfig, metricAggs, state }: ParamIns } // if field param exists, compute allowed fields if (param.type === 'field') { - const availableFields: Field[] = (param as IFieldParamType).getAvailableFields( - agg.getIndexPattern().fields - ); + const availableFields: Field[] = (param as IFieldParamType).getAvailableFields(agg); fields = aggTypeFieldFilters.filter(availableFields, agg); indexedFields = groupAndSortBy(fields, 'type', 'name'); diff --git a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts index a700995ec596b..b7fd6b1e9ebb6 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts @@ -49,7 +49,6 @@ export { AggParamOption } from 'ui/agg_types'; export { CidrMask } from 'ui/agg_types'; export { PersistedState } from 'ui/persisted_state'; -export { IndexedArray } from 'ui/indexed_array'; export { getDocLink } from 'ui/documentation_links'; export { documentationLinks } from 'ui/documentation_links/documentation_links'; export { move } from 'ui/utils/collection'; diff --git a/src/legacy/ui/public/chrome/api/nav.ts b/src/legacy/ui/public/chrome/api/nav.ts index 771314d9e1481..ae32473e451b7 100644 --- a/src/legacy/ui/public/chrome/api/nav.ts +++ b/src/legacy/ui/public/chrome/api/nav.ts @@ -146,7 +146,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) { // link.active and link.lastUrl properties coreNavLinks .getAll() - .filter(link => link.subUrlBase) + .filter(link => link.subUrlBase && !link.disableSubUrlTracking) .forEach(link => { coreNavLinks.update(link.id, { subUrlBase: relativeToAbsolute(chrome.addBasePath(link.subUrlBase)), diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 3d902ebbb7c23..ecddd893d1a54 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -44,7 +44,7 @@ export const indexPatterns = { isDefault, }; -export { Field, FieldList, IFieldList } from './fields'; +export { Field, FieldList } from './fields'; // TODO: figure out how to replace IndexPatterns in get_inner_angular. export { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index 919115d40b068..f21a1610f29e2 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -19,7 +19,12 @@ // eslint-disable-next-line max-classes-per-file import { IndexPatterns } from './index_patterns'; -import { SavedObjectsClientContract, IUiSettingsClient, HttpSetup } from 'kibana/public'; +import { + SavedObjectsClientContract, + IUiSettingsClient, + HttpSetup, + SavedObjectsFindResponsePublic, +} from 'kibana/public'; jest.mock('./index_pattern', () => { class IndexPattern { @@ -45,9 +50,17 @@ jest.mock('./index_patterns_api_client', () => { describe('IndexPatterns', () => { let indexPatterns: IndexPatterns; + let savedObjectsClient: SavedObjectsClientContract; beforeEach(() => { - const savedObjectsClient = {} as SavedObjectsClientContract; + savedObjectsClient = {} as SavedObjectsClientContract; + savedObjectsClient.find = jest.fn( + () => + Promise.resolve({ + savedObjects: [{ id: 'id', attributes: { title: 'title' } }], + }) as Promise> + ); + const uiSettings = {} as IUiSettingsClient; const http = {} as HttpSetup; @@ -61,4 +74,27 @@ describe('IndexPatterns', () => { expect(indexPattern).toBeDefined(); expect(indexPattern).toBe(await indexPatterns.get(id)); }); + + test('savedObjectCache pre-fetches only title', async () => { + expect(await indexPatterns.getIds()).toEqual(['id']); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); + }); + + test('caches saved objects', async () => { + await indexPatterns.getIds(); + await indexPatterns.getTitles(); + await indexPatterns.getFields(['id', 'title']); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + }); + + test('can refresh the saved objects caches', async () => { + await indexPatterns.getIds(); + await indexPatterns.getTitles(true); + await indexPatterns.getFields(['id', 'title'], true); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); + }); }); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index 8d7dd0f054366..2c93ed7fb79bf 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -30,6 +30,8 @@ import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_c const indexPatternCache = createIndexPatternCache(); +type IndexPatternCachedFieldType = 'id' | 'title'; + export class IndexPatterns { private config: IUiSettingsClient; private savedObjectsClient: SavedObjectsClientContract; @@ -50,7 +52,7 @@ export class IndexPatterns { this.savedObjectsCache = ( await this.savedObjectsClient.find({ type: 'index-pattern', - fields: [], + fields: ['title'], perPage: 10000, }) ).savedObjects; @@ -76,7 +78,7 @@ export class IndexPatterns { return this.savedObjectsCache.map(obj => obj?.attributes?.title); }; - getFields = async (fields: string[], refresh: boolean = false) => { + getFields = async (fields: IndexPatternCachedFieldType[], refresh: boolean = false) => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); } @@ -84,8 +86,10 @@ export class IndexPatterns { return []; } return this.savedObjectsCache.map((obj: Record) => { - const result: Record = {}; - fields.forEach((f: string) => (result[f] = obj[f] || obj?.attributes?.[f])); + const result: Partial> = {}; + fields.forEach( + (f: IndexPatternCachedFieldType) => (result[f] = obj[f] || obj?.attributes?.[f]) + ); return result; }); }; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 847d79fdc87d1..726cd6cfb18f0 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -101,6 +101,8 @@ const createStartContract = (): Start => { return startContract; }; +export { searchSourceMock } from './search/mocks'; + export const dataPluginMock = { createSetupContract, createStartContract, diff --git a/src/plugins/data/public/query/state_sync/index.ts b/src/plugins/data/public/query/state_sync/index.ts index 7eefda0d0aec1..27e02940765cf 100644 --- a/src/plugins/data/public/query/state_sync/index.ts +++ b/src/plugins/data/public/query/state_sync/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { syncQuery } from './sync_query'; +export { syncQuery, getQueryStateContainer } from './sync_query'; export { syncAppFilters } from './sync_app_filters'; diff --git a/src/plugins/data/public/query/state_sync/sync_query.test.ts b/src/plugins/data/public/query/state_sync/sync_query.test.ts index 0973af13cacd5..4796da4f5fd4b 100644 --- a/src/plugins/data/public/query/state_sync/sync_query.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_query.test.ts @@ -31,7 +31,7 @@ import { import { QueryService, QueryStart } from '../query_service'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { TimefilterContract } from '../timefilter'; -import { QuerySyncState, syncQuery } from './sync_query'; +import { getQueryStateContainer, QuerySyncState, syncQuery } from './sync_query'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -163,4 +163,69 @@ describe('sync_query', () => { expect(spy).not.toBeCalled(); stop(); }); + + describe('getQueryStateContainer', () => { + test('state is initialized with state from query service', () => { + const { stop, querySyncStateContainer, initialState } = getQueryStateContainer( + queryServiceStart + ); + expect(querySyncStateContainer.getState()).toMatchInlineSnapshot(` + Object { + "filters": Array [], + "refreshInterval": Object { + "pause": true, + "value": 0, + }, + "time": Object { + "from": "now-15m", + "to": "now", + }, + } + `); + expect(initialState).toEqual(querySyncStateContainer.getState()); + stop(); + }); + + test('state takes initial overrides into account', () => { + const { stop, querySyncStateContainer, initialState } = getQueryStateContainer( + queryServiceStart, + { + time: { from: 'now-99d', to: 'now' }, + } + ); + expect(querySyncStateContainer.getState().time).toEqual({ + from: 'now-99d', + to: 'now', + }); + expect(initialState).toEqual(querySyncStateContainer.getState()); + stop(); + }); + + test('when filters change, state container contains updated global filters', () => { + const { stop, querySyncStateContainer } = getQueryStateContainer(queryServiceStart); + filterManager.setFilters([gF, aF]); + expect(querySyncStateContainer.getState().filters).toHaveLength(1); + stop(); + }); + + test('when time range changes, state container contains updated time range', () => { + const { stop, querySyncStateContainer } = getQueryStateContainer(queryServiceStart); + timefilter.setTime({ from: 'now-30m', to: 'now' }); + expect(querySyncStateContainer.getState().time).toEqual({ + from: 'now-30m', + to: 'now', + }); + stop(); + }); + + test('when refresh interval changes, state container contains updated refresh interval', () => { + const { stop, querySyncStateContainer } = getQueryStateContainer(queryServiceStart); + timefilter.setRefreshInterval({ pause: true, value: 100 }); + expect(querySyncStateContainer.getState().refreshInterval).toEqual({ + pause: true, + value: 100, + }); + stop(); + }); + }); }); diff --git a/src/plugins/data/public/query/state_sync/sync_query.ts b/src/plugins/data/public/query/state_sync/sync_query.ts index be641e89f9b76..9a4e9cbba2990 100644 --- a/src/plugins/data/public/query/state_sync/sync_query.ts +++ b/src/plugins/data/public/query/state_sync/sync_query.ts @@ -27,7 +27,7 @@ import { } from '../../../../kibana_utils/public'; import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters'; import { esFilters, RefreshInterval, TimeRange } from '../../../common'; -import { QueryStart } from '../query_service'; +import { QuerySetup, QueryStart } from '../query_service'; const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -40,16 +40,11 @@ export interface QuerySyncState { /** * Helper utility to set up syncing between query services and url's '_g' query param */ -export const syncQuery = ( - { timefilter: { timefilter }, filterManager }: QueryStart, - urlStateStorage: IKbnUrlStateStorage -) => { - const defaultState: QuerySyncState = { - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - filters: filterManager.getGlobalFilters(), - }; - +export const syncQuery = (queryStart: QueryStart, urlStateStorage: IKbnUrlStateStorage) => { + const { + timefilter: { timefilter }, + filterManager, + } = queryStart; // retrieve current state from `_g` url const initialStateFromUrl = urlStateStorage.get(GLOBAL_STATE_STORAGE_KEY); @@ -58,10 +53,82 @@ export const syncQuery = ( initialStateFromUrl && Object.keys(initialStateFromUrl).length ); - // prepare initial state, whatever was in URL takes precedences over current state in services + const { + querySyncStateContainer, + stop: stopPullQueryState, + initialState, + } = getQueryStateContainer(queryStart, initialStateFromUrl || {}); + + const pushQueryStateSubscription = querySyncStateContainer.state$.subscribe( + ({ time, filters: globalFilters, refreshInterval }) => { + // cloneDeep is required because services are mutating passed objects + // and state in state container is frozen + if (time && !_.isEqual(time, timefilter.getTime())) { + timefilter.setTime(_.cloneDeep(time)); + } + + if (refreshInterval && !_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { + timefilter.setRefreshInterval(_.cloneDeep(refreshInterval)); + } + + if ( + globalFilters && + !compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS) + ) { + filterManager.setGlobalFilters(_.cloneDeep(globalFilters)); + } + } + ); + + // if there weren't any initial state in url, + // then put _g key into url + if (!initialStateFromUrl) { + urlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { + replace: true, + }); + } + + // trigger initial syncing from state container to services if needed + querySyncStateContainer.set(initialState); + + const { start, stop: stopSyncState } = syncState({ + stateStorage: urlStateStorage, + stateContainer: { + ...querySyncStateContainer, + set: state => { + if (state) { + // syncState utils requires to handle incoming "null" value + querySyncStateContainer.set(state); + } + }, + }, + storageKey: GLOBAL_STATE_STORAGE_KEY, + }); + + start(); + return { + stop: () => { + stopSyncState(); + pushQueryStateSubscription.unsubscribe(); + stopPullQueryState(); + }, + hasInheritedQueryFromUrl, + }; +}; + +export const getQueryStateContainer = ( + { timefilter: { timefilter }, filterManager }: QuerySetup, + initialStateOverrides: Partial = {} +) => { + const defaultState: QuerySyncState = { + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + filters: filterManager.getGlobalFilters(), + }; + const initialState: QuerySyncState = { ...defaultState, - ...initialStateFromUrl, + ...initialStateOverrides, }; // create state container, which will be used for syncing with syncState() util @@ -109,59 +176,13 @@ export const syncQuery = ( .subscribe(newGlobalFilters => { querySyncStateContainer.transitions.setFilters(newGlobalFilters); }), - querySyncStateContainer.state$.subscribe( - ({ time, filters: globalFilters, refreshInterval }) => { - // cloneDeep is required because services are mutating passed objects - // and state in state container is frozen - if (time && !_.isEqual(time, timefilter.getTime())) { - timefilter.setTime(_.cloneDeep(time)); - } - - if (refreshInterval && !_.isEqual(refreshInterval, timefilter.getRefreshInterval())) { - timefilter.setRefreshInterval(_.cloneDeep(refreshInterval)); - } - - if ( - globalFilters && - !compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS) - ) { - filterManager.setGlobalFilters(_.cloneDeep(globalFilters)); - } - } - ), ]; - // if there weren't any initial state in url, - // then put _g key into url - if (!initialStateFromUrl) { - urlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, initialState, { - replace: true, - }); - } - - // trigger initial syncing from state container to services if needed - querySyncStateContainer.set(initialState); - - const { start, stop } = syncState({ - stateStorage: urlStateStorage, - stateContainer: { - ...querySyncStateContainer, - set: state => { - if (state) { - // syncState utils requires to handle incoming "null" value - querySyncStateContainer.set(state); - } - }, - }, - storageKey: GLOBAL_STATE_STORAGE_KEY, - }); - - start(); return { + querySyncStateContainer, stop: () => { subs.forEach(s => s.unsubscribe()); - stop(); }, - hasInheritedQueryFromUrl, + initialState, }; }; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 81a028007bc94..821bd45f731e8 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -17,6 +17,8 @@ * under the License. */ +export * from './search_source/mocks'; + export const searchSetupMock = { registerSearchStrategyContext: jest.fn(), registerSearchStrategyProvider: jest.fn(), diff --git a/src/plugins/home/server/tutorials/instructions/instruction_variant.ts b/src/plugins/home/common/instruction_variant.ts similarity index 100% rename from src/plugins/home/server/tutorials/instructions/instruction_variant.ts rename to src/plugins/home/common/instruction_variant.ts diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 114d442b40943..2a445cf242729 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -26,6 +26,7 @@ export { HomePublicPluginStart, } from './plugin'; export { FeatureCatalogueEntry, FeatureCatalogueCategory, Environment } from './services'; +export * from '../common/instruction_variant'; import { HomePublicPlugin } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 02f4c91a414cc..75ace84344216 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -36,5 +36,5 @@ export const config: PluginConfigDescriptor = { export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext); -export { INSTRUCTION_VARIANT } from './tutorials/instructions/instruction_variant'; +export { INSTRUCTION_VARIANT } from '../common/instruction_variant'; export { ArtifactsSchema, TutorialsCategory } from './services/tutorials'; diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index 6a9dba69b193f..4c85ad3985b3d 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index 176a3901821f1..66efa36ec9bcd 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index 385880ba9780f..ee13b9c5eefd8 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index 406bf55da4321..33f5defc0273f 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index 77efe0958a615..9fdc70e0703a4 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index cc18f2ce9705d..9d7d0660d3d6c 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from './instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; diff --git a/src/plugins/home/server/tutorials/netflow/elastic_cloud.ts b/src/plugins/home/server/tutorials/netflow/elastic_cloud.ts index ac64aef730004..fbedc6abfbb8a 100644 --- a/src/plugins/home/server/tutorials/netflow/elastic_cloud.ts +++ b/src/plugins/home/server/tutorials/netflow/elastic_cloud.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../instructions/instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createLogstashInstructions } from '../instructions/logstash_instructions'; import { createCommonNetflowInstructions } from './common_instructions'; diff --git a/src/plugins/home/server/tutorials/netflow/on_prem.ts b/src/plugins/home/server/tutorials/netflow/on_prem.ts index c7cd36d073632..ef8c3e172af87 100644 --- a/src/plugins/home/server/tutorials/netflow/on_prem.ts +++ b/src/plugins/home/server/tutorials/netflow/on_prem.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../instructions/instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createLogstashInstructions } from '../instructions/logstash_instructions'; import { createCommonNetflowInstructions } from './common_instructions'; diff --git a/src/plugins/home/server/tutorials/netflow/on_prem_elastic_cloud.ts b/src/plugins/home/server/tutorials/netflow/on_prem_elastic_cloud.ts index c01a9a5382f88..85aa694970491 100644 --- a/src/plugins/home/server/tutorials/netflow/on_prem_elastic_cloud.ts +++ b/src/plugins/home/server/tutorials/netflow/on_prem_elastic_cloud.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../instructions/instruction_variant'; +import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; import { createLogstashInstructions } from '../instructions/logstash_instructions'; import { createTrycloudOption1, diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index b9a61a1c9b200..7c4b3428cbb6d 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -17,8 +17,8 @@ * under the License. */ -import { App, PluginInitializerContext } from 'kibana/public'; - +import { App, AppBase, PluginInitializerContext, AppUpdatableFields } from 'kibana/public'; +import { Observable } from 'rxjs'; import { ConfigSchema } from '../config'; interface ForwardDefinition { @@ -27,8 +27,26 @@ interface ForwardDefinition { keepPrefix: boolean; } +export type AngularRenderedAppUpdater = ( + app: AppBase +) => Partial | undefined; + +export interface AngularRenderedApp extends App { + /** + * Angular rendered apps are able to update the active url in the nav link (which is currently not + * possible for actual NP apps). When regular applications have the same functionality, this type + * override can be removed. + */ + updater$?: Observable; + /** + * If the active url is updated via the updater$ subject, the app id is assumed to be identical with + * the nav link id. If this is not the case, it is possible to provide another nav link id here. + */ + navLinkId?: string; +} + export class KibanaLegacyPlugin { - private apps: App[] = []; + private apps: AngularRenderedApp[] = []; private forwards: ForwardDefinition[] = []; constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -52,7 +70,7 @@ export class KibanaLegacyPlugin { * * @param app The app descriptor */ - registerLegacyApp: (app: App) => { + registerLegacyApp: (app: AngularRenderedApp) => { this.apps.push(app); }, diff --git a/src/plugins/kibana_react/public/adapters/index.ts b/src/plugins/kibana_react/public/adapters/index.ts new file mode 100644 index 0000000000000..9912967022793 --- /dev/null +++ b/src/plugins/kibana_react/public/adapters/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './react_to_ui_component'; +export * from './ui_to_react_component'; diff --git a/src/plugins/kibana_react/public/adapters/react_to_ui_component.test.tsx b/src/plugins/kibana_react/public/adapters/react_to_ui_component.test.tsx new file mode 100644 index 0000000000000..14fe02c6bf652 --- /dev/null +++ b/src/plugins/kibana_react/public/adapters/react_to_ui_component.test.tsx @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as React from 'react'; +import { reactToUiComponent } from './react_to_ui_component'; + +const ReactComp: React.FC<{ cnt?: number }> = ({ cnt = 0 }) => { + return
cnt: {cnt}
; +}; + +describe('reactToUiComponent', () => { + test('can render UI component', () => { + const UiComp = reactToUiComponent(ReactComp); + const div = document.createElement('div'); + + const instance = UiComp(); + instance.render(div, {}); + + expect(div.innerHTML).toBe('
cnt: 0
'); + }); + + test('can pass in props', async () => { + const UiComp = reactToUiComponent(ReactComp); + const div = document.createElement('div'); + + const instance = UiComp(); + instance.render(div, { cnt: 5 }); + + expect(div.innerHTML).toBe('
cnt: 5
'); + }); + + test('can re-render multiple times', async () => { + const UiComp = reactToUiComponent(ReactComp); + const div = document.createElement('div'); + const instance = UiComp(); + + instance.render(div, { cnt: 1 }); + + expect(div.innerHTML).toBe('
cnt: 1
'); + + instance.render(div, { cnt: 2 }); + + expect(div.innerHTML).toBe('
cnt: 2
'); + }); + + test('renders React component only when .render() method is called', () => { + let renderCnt = 0; + const MyReactComp: React.FC<{ cnt?: number }> = ({ cnt = 0 }) => { + renderCnt++; + return
cnt: {cnt}
; + }; + const UiComp = reactToUiComponent(MyReactComp); + const instance = UiComp(); + const div = document.createElement('div'); + + expect(renderCnt).toBe(0); + + instance.render(div, { cnt: 1 }); + + expect(renderCnt).toBe(1); + + instance.render(div, { cnt: 2 }); + + expect(renderCnt).toBe(2); + + instance.render(div, { cnt: 3 }); + + expect(renderCnt).toBe(3); + }); +}); diff --git a/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts b/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts new file mode 100644 index 0000000000000..b4007b30cf8ca --- /dev/null +++ b/src/plugins/kibana_react/public/adapters/react_to_ui_component.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ComponentType, createElement as h } from 'react'; +import { render as renderReact, unmountComponentAtNode } from 'react-dom'; +import { UiComponent, UiComponentInstance } from '../../../kibana_utils/common'; + +/** + * Transform a React component into a `UiComponent`. + * + * @param ReactComp A React component. + */ +export const reactToUiComponent = ( + ReactComp: ComponentType +): UiComponent => () => { + let lastEl: HTMLElement | undefined; + + const render: UiComponentInstance['render'] = (el, props) => { + lastEl = el; + renderReact(h(ReactComp, props), el); + }; + + const unmount: UiComponentInstance['unmount'] = () => { + if (lastEl) unmountComponentAtNode(lastEl); + }; + + const comp: UiComponentInstance = { + render, + unmount, + }; + + return comp; +}; diff --git a/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx b/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx new file mode 100644 index 0000000000000..939d372b9997f --- /dev/null +++ b/src/plugins/kibana_react/public/adapters/ui_to_react_component.test.tsx @@ -0,0 +1,149 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { UiComponent } from '../../../kibana_utils/common'; +import { uiToReactComponent } from './ui_to_react_component'; +import { reactToUiComponent } from './react_to_ui_component'; + +const UiComp: UiComponent<{ cnt?: number }> = () => ({ + render: (el, { cnt = 0 }) => { + el.innerHTML = `cnt: ${cnt}`; + }, +}); + +describe('uiToReactComponent', () => { + test('can render React component', () => { + const ReactComp = uiToReactComponent(UiComp); + const div = document.createElement('div'); + + ReactDOM.render(, div); + + expect(div.innerHTML).toBe('
cnt: 0
'); + }); + + test('can pass in props', async () => { + const ReactComp = uiToReactComponent(UiComp); + const div = document.createElement('div'); + + ReactDOM.render(, div); + + expect(div.innerHTML).toBe('
cnt: 5
'); + }); + + test('re-renders when React component is re-rendered', async () => { + const ReactComp = uiToReactComponent(UiComp); + const div = document.createElement('div'); + + ReactDOM.render(, div); + + expect(div.innerHTML).toBe('
cnt: 1
'); + + ReactDOM.render(, div); + + expect(div.innerHTML).toBe('
cnt: 2
'); + }); + + test('does not crash if .unmount() not provided', () => { + const UiComp2: UiComponent<{ cnt?: number }> = () => ({ + render: (el, { cnt = 0 }) => { + el.innerHTML = `cnt: ${cnt}`; + }, + }); + const ReactComp = uiToReactComponent(UiComp2); + const div = document.createElement('div'); + + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); + + expect(div.innerHTML).toBe(''); + }); + + test('calls .unmount() method once when component un-mounts', () => { + const unmount = jest.fn(); + const UiComp2: UiComponent<{ cnt?: number }> = () => ({ + render: (el, { cnt = 0 }) => { + el.innerHTML = `cnt: ${cnt}`; + }, + unmount, + }); + const ReactComp = uiToReactComponent(UiComp2); + const div = document.createElement('div'); + + expect(unmount).toHaveBeenCalledTimes(0); + + ReactDOM.render(, div); + + expect(unmount).toHaveBeenCalledTimes(0); + + ReactDOM.unmountComponentAtNode(div); + + expect(unmount).toHaveBeenCalledTimes(1); + }); + + test('calls .render() method only once when components mounts, and once on every re-render', () => { + const render = jest.fn((el, { cnt = 0 }) => { + el.innerHTML = `cnt: ${cnt}`; + }); + const UiComp2: UiComponent<{ cnt?: number }> = () => ({ + render, + }); + const ReactComp = uiToReactComponent(UiComp2); + const div = document.createElement('div'); + + expect(render).toHaveBeenCalledTimes(0); + + ReactDOM.render(, div); + + expect(render).toHaveBeenCalledTimes(1); + + ReactDOM.render(, div); + + expect(render).toHaveBeenCalledTimes(2); + + ReactDOM.render(, div); + + expect(render).toHaveBeenCalledTimes(3); + }); + + test('can specify wrapper element', async () => { + const ReactComp = uiToReactComponent(UiComp, 'span'); + const div = document.createElement('div'); + + ReactDOM.render(, div); + + expect(div.innerHTML).toBe('cnt: 5'); + }); +}); + +test('can adapt component many times', () => { + const ReactComp = uiToReactComponent( + reactToUiComponent(uiToReactComponent(reactToUiComponent(uiToReactComponent(UiComp)))) + ); + const div = document.createElement('div'); + + ReactDOM.render(, div); + + expect(div.textContent).toBe('cnt: 0'); + + ReactDOM.render(, div); + + expect(div.textContent).toBe('cnt: 123'); +}); diff --git a/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts b/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts new file mode 100644 index 0000000000000..9b34880cf4fe3 --- /dev/null +++ b/src/plugins/kibana_react/public/adapters/ui_to_react_component.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FC, createElement as h, useRef, useLayoutEffect, useMemo } from 'react'; +import { UiComponent, UiComponentInstance } from '../../../kibana_utils/common'; + +/** + * Transforms `UiComponent` into a React component. + */ +export const uiToReactComponent = ( + Comp: UiComponent, + as: string = 'div' +): FC => props => { + const ref = useRef(); + const comp = useMemo>(() => Comp(), [Comp]); + + useLayoutEffect(() => { + if (!ref.current) return; + comp.render(ref.current, props); + }); + + useLayoutEffect(() => { + if (!comp.unmount) return; + return () => { + if (comp.unmount) comp.unmount(); + }; + }, [comp]); + + return h(as, { + ref, + }); +}; diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index 81f2e694e8e5b..e1f90b9c60199 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -26,5 +26,6 @@ export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; export * from './split_panel'; +export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; export { toMountPoint } from './util'; diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 444c10194a8e3..fb608a0db1ac2 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -19,6 +19,7 @@ export * from './defer'; export * from './of'; +export * from './ui'; export * from './state_containers'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/common/ui/index.ts b/src/plugins/kibana_utils/common/ui/index.ts new file mode 100644 index 0000000000000..0cfb2f13c8a5d --- /dev/null +++ b/src/plugins/kibana_utils/common/ui/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './ui_component'; diff --git a/src/plugins/kibana_utils/common/ui/ui_component.ts b/src/plugins/kibana_utils/common/ui/ui_component.ts new file mode 100644 index 0000000000000..6984ab9b78e5f --- /dev/null +++ b/src/plugins/kibana_utils/common/ui/ui_component.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * In many places in Kibana we want to be agnostic to frontend view library, + * i.e. instead of exposing React-specific APIs we want to expose APIs that + * are orthogonal to any rendering library. This interface represents such UI + * components. UI component receives a DOM element and `props` through `render()` + * method, the `render()` method can be called many times. + * + * Although Kibana aims to be library agnostic, Kibana itself is written in React, + * thus here we define `UiComponent` which is an abstract unit of UI that can be + * implemented in any framework, but it maps easily to React components, i.e. + * `UiComponent` is like `React.ComponentType`. + */ +export type UiComponent = () => UiComponentInstance; + +/** + * Instance of an UiComponent, corresponds to React virtual DOM node. + */ +export interface UiComponentInstance { + /** + * Call this method on initial render and on all subsequent updates. + * + * @param el DOM element. + * @param props Component props, same as props in React. + */ + render(el: HTMLElement, props: Props): void; + + /** + * Un-mount UI component. Call it to remove view from DOM. Implementers of this + * interface should clear DOM from this UI component and destroy any internal state. + */ + unmount?(): void; +} diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 5b6d304e14c2e..883f28da45223 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -17,7 +17,16 @@ * under the License. */ -export { defer, Defer, of, createGetterSetter, Get, Set } from '../common'; +export { + defer, + Defer, + of, + createGetterSetter, + Get, + Set, + UiComponent, + UiComponentInstance, +} from '../common'; export * from './core'; export * from './errors'; export * from './field_mapping'; @@ -40,6 +49,7 @@ export { unhashUrl, unhashQuery, createUrlTracker, + createKbnUrlTracker, createKbnUrlControls, getStateFromKbnUrl, getStatesFromKbnUrl, diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts index 40491bf7a274b..e28d183c6560a 100644 --- a/src/plugins/kibana_utils/public/state_management/url/index.ts +++ b/src/plugins/kibana_utils/public/state_management/url/index.ts @@ -25,4 +25,5 @@ export { getStatesFromKbnUrl, IKbnUrlControls, } from './kbn_url_storage'; +export { createKbnUrlTracker } from './kbn_url_tracker'; export { createUrlTracker } from './url_tracker'; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts new file mode 100644 index 0000000000000..4b17d8517328b --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts @@ -0,0 +1,184 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; +import { createMemoryHistory, History } from 'history'; +import { createKbnUrlTracker, KbnUrlTracker } from './kbn_url_tracker'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { AppBase, ToastsSetup } from 'kibana/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { unhashUrl } from './hash_unhash_url'; + +jest.mock('./hash_unhash_url', () => ({ + unhashUrl: jest.fn(x => x), +})); + +describe('kbnUrlTracker', () => { + let storage: StubBrowserStorage; + let history: History; + let urlTracker: KbnUrlTracker; + let state1Subject: Subject<{ key1: string }>; + let state2Subject: Subject<{ key2: string }>; + let navLinkUpdaterSubject: BehaviorSubject<(app: AppBase) => { activeUrl?: string } | undefined>; + let toastService: jest.Mocked; + + function createTracker() { + urlTracker = createKbnUrlTracker({ + baseUrl: '/app/test', + defaultSubUrl: '#/start', + storageKey: 'storageKey', + history, + storage, + stateParams: [ + { + kbnUrlKey: 'state1', + stateUpdate$: state1Subject.asObservable(), + }, + { + kbnUrlKey: 'state2', + stateUpdate$: state2Subject.asObservable(), + }, + ], + navLinkUpdater$: navLinkUpdaterSubject, + toastNotifications: toastService, + }); + } + + function getActiveNavLinkUrl() { + return navLinkUpdaterSubject.getValue()({} as AppBase)?.activeUrl; + } + + beforeEach(() => { + jest.clearAllMocks(); + toastService = coreMock.createSetup().notifications.toasts; + storage = new StubBrowserStorage(); + history = createMemoryHistory(); + state1Subject = new Subject<{ key1: string }>(); + state2Subject = new Subject<{ key2: string }>(); + navLinkUpdaterSubject = new BehaviorSubject< + (app: AppBase) => { activeUrl?: string } | undefined + >(() => undefined); + }); + + test('do not touch nav link to default if nothing else is set', () => { + createTracker(); + expect(getActiveNavLinkUrl()).toEqual(undefined); + }); + + test('set nav link to session storage value if defined', () => { + storage.setItem('storageKey', '#/deep/path'); + createTracker(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path'); + }); + + test('set nav link to default if app gets mounted', () => { + storage.setItem('storageKey', '#/deep/path'); + createTracker(); + urlTracker.appMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); + }); + + test('keep nav link to default if path gets changed while app mounted', () => { + storage.setItem('storageKey', '#/deep/path'); + createTracker(); + urlTracker.appMounted(); + history.push('/deep/path/2'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); + }); + + test('change nav link to last visited url within app after unmount', () => { + createTracker(); + urlTracker.appMounted(); + history.push('/deep/path/2'); + history.push('/deep/path/3'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3'); + }); + + test('unhash all urls that are recorded while app is mounted', () => { + (unhashUrl as jest.Mock).mockImplementation(x => x + '?unhashed'); + createTracker(); + urlTracker.appMounted(); + history.push('/deep/path/2'); + history.push('/deep/path/3'); + urlTracker.appUnMounted(); + expect(unhashUrl).toHaveBeenCalledTimes(2); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3?unhashed'); + }); + + test('show warning and use hashed url if unhashing does not work', () => { + (unhashUrl as jest.Mock).mockImplementation(() => { + throw new Error('unhash broke'); + }); + createTracker(); + urlTracker.appMounted(); + history.push('/deep/path/2'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/2'); + expect(toastService.addDanger).toHaveBeenCalledWith('unhash broke'); + }); + + test('change nav link back to default if app gets mounted again', () => { + createTracker(); + urlTracker.appMounted(); + history.push('/deep/path/2'); + history.push('/deep/path/3'); + urlTracker.appUnMounted(); + urlTracker.appMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); + }); + + test('update state param when app is not mounted', () => { + createTracker(); + state1Subject.next({ key1: 'abc' }); + expect(getActiveNavLinkUrl()).toMatchInlineSnapshot(`"/app/test#/start?state1=(key1:abc)"`); + }); + + test('update state param without overwriting rest of the url when app is not mounted', () => { + storage.setItem('storageKey', '#/deep/path?extrastate=1'); + createTracker(); + state1Subject.next({ key1: 'abc' }); + expect(getActiveNavLinkUrl()).toMatchInlineSnapshot( + `"/app/test#/deep/path?extrastate=1&state1=(key1:abc)"` + ); + }); + + test('not update state param when app is mounted', () => { + createTracker(); + urlTracker.appMounted(); + state1Subject.next({ key1: 'abc' }); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); + }); + + test('update state param multiple times when app is not mounted', () => { + createTracker(); + state1Subject.next({ key1: 'abc' }); + state1Subject.next({ key1: 'def' }); + expect(getActiveNavLinkUrl()).toMatchInlineSnapshot(`"/app/test#/start?state1=(key1:def)"`); + }); + + test('update multiple state params when app is not mounted', () => { + createTracker(); + state1Subject.next({ key1: 'abc' }); + state2Subject.next({ key2: 'def' }); + expect(getActiveNavLinkUrl()).toMatchInlineSnapshot( + `"/app/test#/start?state1=(key1:abc)&state2=(key2:def)"` + ); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts new file mode 100644 index 0000000000000..6f3f64ea7b941 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts @@ -0,0 +1,192 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createHashHistory, History, UnregisterCallback } from 'history'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { AppBase, ToastsSetup } from 'kibana/public'; +import { setStateToKbnUrl } from './kbn_url_storage'; +import { unhashUrl } from './hash_unhash_url'; + +export interface KbnUrlTracker { + /** + * Callback to invoke when the app is mounted + */ + appMounted: () => void; + /** + * Callback to invoke when the app is unmounted + */ + appUnMounted: () => void; + /** + * Unregistering the url tracker. This won't reset the current state of the nav link + */ + stop: () => void; +} + +/** + * Listens to history changes and optionally to global state changes and updates the nav link url of + * a given app to point to the last visited page within the app. + * + * This includes the following parts: + * * When the app is currently active, the nav link points to the configurable default url of the app. + * * When the app is not active the last visited url is set to the nav link. + * * When a provided observable emits a new value, the state parameter in the url of the nav link is updated + * as long as the app is not active. + */ +export function createKbnUrlTracker({ + baseUrl, + defaultSubUrl, + storageKey, + stateParams, + navLinkUpdater$, + toastNotifications, + history, + storage, +}: { + /** + * Base url of the current app. This will be used as a prefix for the + * nav link in the side bar + */ + baseUrl: string; + /** + * Default sub url for this app. If the app is currently active or no sub url is already stored in session storage and the app hasn't been visited yet, the nav link will be set to this url. + */ + defaultSubUrl: string; + /** + * List of URL mapped states that should get updated even when the app is not currently active + */ + stateParams: Array<{ + /** + * Key of the query parameter containing the state + */ + kbnUrlKey: string; + /** + * Observable providing updates to the state + */ + stateUpdate$: Observable; + }>; + /** + * Key used to store the current sub url in session storage. This key should only be used for one active url tracker at any given ntime. + */ + storageKey: string; + /** + * App updater subject passed into the application definition to change nav link url. + */ + navLinkUpdater$: BehaviorSubject<(app: AppBase) => { activeUrl?: string } | undefined>; + /** + * Toast notifications service to show toasts in error cases. + */ + toastNotifications: ToastsSetup; + /** + * History object to use to track url changes. If this isn't provided, a local history instance will be created. + */ + history?: History; + /** + * Storage object to use to persist currently active url. If this isn't provided, the browser wide session storage instance will be used. + */ + storage?: Storage; +}): KbnUrlTracker { + const historyInstance = history || createHashHistory(); + const storageInstance = storage || sessionStorage; + + // local state storing current listeners and active url + let activeUrl: string = ''; + let unsubscribeURLHistory: UnregisterCallback | undefined; + let unsubscribeGlobalState: Subscription[] | undefined; + + function setNavLink(hash: string) { + navLinkUpdater$.next(() => ({ activeUrl: baseUrl + hash })); + } + + function getActiveSubUrl(url: string) { + // remove baseUrl prefix (just storing the sub url part) + return url.substr(baseUrl.length); + } + + function unsubscribe() { + if (unsubscribeURLHistory) { + unsubscribeURLHistory(); + unsubscribeURLHistory = undefined; + } + + if (unsubscribeGlobalState) { + unsubscribeGlobalState.forEach(sub => sub.unsubscribe()); + unsubscribeGlobalState = undefined; + } + } + + function onMountApp() { + unsubscribe(); + // track current hash when within app + unsubscribeURLHistory = historyInstance.listen(location => { + const urlWithHashes = baseUrl + '#' + location.pathname + location.search; + let urlWithStates = ''; + try { + urlWithStates = unhashUrl(urlWithHashes); + } catch (e) { + toastNotifications.addDanger(e.message); + } + + activeUrl = getActiveSubUrl(urlWithStates || urlWithHashes); + storageInstance.setItem(storageKey, activeUrl); + }); + } + + function onUnmountApp() { + unsubscribe(); + // propagate state updates when in other apps + unsubscribeGlobalState = stateParams.map(({ stateUpdate$, kbnUrlKey }) => + stateUpdate$.subscribe(state => { + const updatedUrl = setStateToKbnUrl( + kbnUrlKey, + state, + { useHash: false }, + baseUrl + (activeUrl || defaultSubUrl) + ); + // remove baseUrl prefix (just storing the sub url part) + activeUrl = getActiveSubUrl(updatedUrl); + storageInstance.setItem(storageKey, activeUrl); + setNavLink(activeUrl); + }) + ); + } + + // register listeners for unmounted app initially + onUnmountApp(); + + // initialize nav link and internal state + const storedUrl = storageInstance.getItem(storageKey); + if (storedUrl) { + activeUrl = storedUrl; + setNavLink(storedUrl); + } + + return { + appMounted() { + onMountApp(); + setNavLink(defaultSubUrl); + }, + appUnMounted() { + onUnmountApp(); + setNavLink(activeUrl); + }, + stop() { + unsubscribe(); + }, + }; +} diff --git a/src/plugins/ui_actions/public/actions/i_action.ts b/src/plugins/ui_actions/public/actions/i_action.ts index 20fdda9033f6a..544b66b26c974 100644 --- a/src/plugins/ui_actions/public/actions/i_action.ts +++ b/src/plugins/ui_actions/public/actions/i_action.ts @@ -17,6 +17,8 @@ * under the License. */ +import { UiComponent } from 'src/plugins/kibana_utils/common'; + export interface IAction { /** * Determined the order when there is more than one action matched to a trigger. @@ -39,6 +41,12 @@ export interface IAction { */ getDisplayName(context: ActionContext): string; + /** + * `UiComponent` to render when displaying this action as a context menu item. + * If not provided, `getDisplayName` will be used instead. + */ + MenuItem?: UiComponent<{ context: ActionContext }>; + /** * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.ts b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx similarity index 89% rename from src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.ts rename to src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 8de447f5acf9c..3b76ff66f3aea 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.ts +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -17,9 +17,11 @@ * under the License. */ +import * as React from 'react'; import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { uiToReactComponent } from '../../../kibana_react/public'; import { IAction } from '../actions'; /** @@ -98,7 +100,12 @@ function convertPanelActionToContextMenuItem({ closeMenu: () => void; }): EuiContextMenuPanelItemDescriptor { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { - name: action.getDisplayName(actionContext), + name: action.MenuItem + ? // Cast to `any` because `name` typed to string. + (React.createElement(uiToReactComponent(action.MenuItem), { + context: actionContext, + }) as any) + : action.getDisplayName(actionContext), icon: action.getIconType(actionContext), panel: _.get(action, 'childContextMenuPanel.id'), 'data-test-subj': `embeddablePanelAction-${action.id}`, diff --git a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx index 9e3d206c9a6dc..4e18b8ec27fb9 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx +++ b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx @@ -18,16 +18,31 @@ */ import React from 'react'; -import { EuiFlyout } from '@elastic/eui'; +import { EuiFlyout, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; import { CoreStart } from 'src/core/public'; import { createAction, IAction } from '../../actions'; -import { toMountPoint } from '../../../../kibana_react/public'; +import { toMountPoint, reactToUiComponent } from '../../../../kibana_react/public'; + +const ReactMenuItem: React.FC = () => { + return ( + + Hello world! + + {'secret'} + + + ); +}; + +const UiMenuItem = reactToUiComponent(ReactMenuItem); export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID'; export function createHelloWorldAction(overlays: CoreStart['overlays']): IAction { return createAction({ type: HELLO_WORLD_ACTION_ID, + getIconType: () => 'lock', + MenuItem: UiMenuItem, execute: async () => { const flyoutSession = overlays.openFlyout( toMountPoint( diff --git a/x-pack/legacy/plugins/apm/common/projections/typings.ts b/x-pack/legacy/plugins/apm/common/projections/typings.ts index 2b55395b70c6b..08a7bee5412a5 100644 --- a/x-pack/legacy/plugins/apm/common/projections/typings.ts +++ b/x-pack/legacy/plugins/apm/common/projections/typings.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchRequest, ESSearchBody } from '../../typings/elasticsearch'; +import { + ESSearchRequest, + ESSearchBody +} from '../../../../../plugins/apm/typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap -} from '../../typings/elasticsearch/aggregations'; +} from '../../../../../plugins/apm/typings/elasticsearch/aggregations'; export type Projection = Omit & { body: Omit & { diff --git a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts index 6a5089733bb33..ef6089872b786 100644 --- a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts @@ -5,11 +5,11 @@ */ import { merge, isPlainObject, cloneDeep } from 'lodash'; import { DeepPartial } from 'utility-types'; -import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; +import { AggregationInputMap } from '../../../../../../../plugins/apm/typings/elasticsearch/aggregations'; import { ESSearchRequest, ESSearchBody -} from '../../../../typings/elasticsearch'; +} from '../../../../../../../plugins/apm/typings/elasticsearch'; import { Projection } from '../../typings'; type PlainObject = Record; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx index 07ea97f442b7f..38e86e4a0d1c9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -11,7 +11,6 @@ import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; import { CytoscapeContext } from './Cytoscape'; import { animationOptions, nodeHeight } from './cytoscapeOptions'; -import { FullscreenPanel } from './FullscreenPanel'; const ControlsContainer = styled('div')` left: ${theme.gutterTypes.gutterMedium}; @@ -87,7 +86,6 @@ export function Controls() { const minZoom = cy.minZoom(); const isMinZoom = zoom === minZoom; const increment = (maxZoom - minZoom) / steps; - const mapDomElement = cy.container(); const zoomInLabel = i18n.translate('xpack.apm.serviceMap.zoomIn', { defaultMessage: 'Zoom in' }); @@ -127,7 +125,6 @@ export function Controls() { title={centerLabel} /> - ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/FullscreenPanel.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/FullscreenPanel.tsx deleted file mode 100644 index 851bf0ebf56fd..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/FullscreenPanel.tsx +++ /dev/null @@ -1,57 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiButtonIcon, EuiPanel } from '@elastic/eui'; -import styled from 'styled-components'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { i18n } from '@kbn/i18n'; - -const Button = styled(EuiButtonIcon)` - display: block; - margin: ${theme.paddingSizes.xs}; -`; - -interface FullscreenPanelProps { - element: Element | null; -} - -export function FullscreenPanel({ element }: FullscreenPanelProps) { - const canDoFullscreen = - element && element.ownerDocument && element.ownerDocument.fullscreenEnabled; - - if (!canDoFullscreen) { - return null; - } - - function doFullscreen() { - if (element && element.ownerDocument && canDoFullscreen) { - const isFullscreen = element.ownerDocument.fullscreenElement !== null; - - if (isFullscreen) { - element.ownerDocument.exitFullscreen(); - } else { - element.requestFullscreen(); - } - } - } - - const label = i18n.translate('xpack.apm.serviceMap.fullscreen', { - defaultMessage: 'Full screen' - }); - - return ( - -