diff --git a/docs/visualize/timelion.asciidoc b/docs/visualize/timelion.asciidoc index 7c7ec7c995ea7..a7520227977bc 100644 --- a/docs/visualize/timelion.asciidoc +++ b/docs/visualize/timelion.asciidoc @@ -43,7 +43,7 @@ image::images/timelion-create01.png[] [[time-series-compare-data]] ==== Compare the data -To compare the two data sets, add another series with data from the previous hour, separated by a comma: +To compare the two data sets, add another series with data from the previous hour, separated by a comma: [source,text] ---------------------------------- @@ -81,7 +81,7 @@ image::images/timelion-create03.png[] [float] [[time-series-title]] -==== Add a title +==== Add a title Add a meaningful title: @@ -169,7 +169,7 @@ Change the position and style of the legend: [source,text] ---------------------------------- -.es(offset=-1h,index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('last hour').lines(fill=1,width=0.5).color(gray), .es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('current hour').title('CPU usage over time').color(#1E90FF).legend(columns=2, position=nw) <1> +.es(offset=-1h,index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('last hour').lines(fill=1,width=0.5).color(gray), .es(index=metricbeat-*, timefield='@timestamp', metric='avg:system.cpu.user.pct').label('current hour').title('CPU usage over time').color(#1E90FF).legend(columns=2, position=nw) <1> ---------------------------------- <1> `.legend()` sets the position and style of the legend. In this example, `.legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. @@ -210,7 +210,7 @@ Change how the data is displayed so that you can easily monitor the inbound traf .es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes).derivative() <1> ---------------------------------- -<1> `.derivative` plots the change in values over time. +<1> `.derivative` plots the change in values over time. [role="screenshot"] image::images/timelion-math02.png[] @@ -240,7 +240,7 @@ To make the visualization easier to analyze, change the data metric from bytes t .es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.in.bytes).derivative().divide(1048576), .es(index=metricbeat*, timefield=@timestamp, metric=max:system.network.out.bytes).derivative().multiply(-1).divide(1048576) <1> ---------------------------------- -<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor. +<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor. [role="screenshot"] image::images/timelion-math04.png[] @@ -256,7 +256,7 @@ Customize and format the visualization using functions: ---------------------------------- .es(index=metricbeat*, timefield=@timestamp, - metric=max:system.network.in.byte) + metric=max:system.network.in.bytes) .derivative() .divide(1048576) .lines(fill=2, width=1) @@ -270,7 +270,7 @@ Customize and format the visualization using functions: .multiply(-1) .divide(1048576) .lines(fill=2, width=1) <3> - .color(blue) < <4> + .color(blue) <4> .label("Outbound traffic") .legend(columns=2, position=nw) <5> ---------------------------------- diff --git a/package.json b/package.json index 9707d3863d295..425527a058e86 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "clean": { "extraPatterns": [ "build", - "optimize", + "data/optimize", "built_assets", ".eslintcache", ".node_binaries" diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 568980f50117d..5517dfa7f9a23 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -14,6 +14,7 @@ APIs to their New Platform equivalents. - [3. New Platform shim using New Platform router](#3-new-platform-shim-using-new-platform-router) - [4. New Platform plugin](#4-new-platform-plugin) - [Accessing Services](#accessing-services) + - [Migrating Hapi "pre" handlers](#migrating-hapi-pre-handlers) - [Chrome](#chrome) - [Updating an application navlink](#updating-application-navlink) - [Chromeless Applications](#chromeless-applications) @@ -450,6 +451,142 @@ class Plugin { } ``` +### Migrating Hapi "pre" handlers + +In the Legacy Platform, routes could provide a "pre" option in their config to +register a function that should be run prior to the route handler. These +"pre" handlers allow routes to share some business logic that may do some +pre-work or validation. In Kibana, these are often used for license checks. + +The Kibana Platform's HTTP interface does not provide this functionality, +however it is simple enough to port over using a higher-order function that can +wrap the route handler. + +#### Simple example + +In this simple example, a pre-handler is used to either abort the request with +an error or continue as normal. This is a simple "gate-keeping" pattern. + +```ts +// Legacy pre-handler +const licensePreRouting = (request) => { + const licenseInfo = getMyPluginLicenseInfo(request.server.plugins.xpack_main); + if (!licenseInfo.isOneOf(['gold', 'platinum', 'trial'])) { + throw Boom.forbidden(`You don't have the right license for MyPlugin!`); + } +} + +server.route({ + method: 'GET', + path: '/api/my-plugin/do-something', + config: { + pre: [{ method: licensePreRouting }] + }, + handler: (req) => { + return doSomethingInteresting(); + } +}) +``` + +In the Kibana Platform, the same functionality can be acheived by creating a +function that takes a route handler (or factory for a route handler) as an +argument and either invokes it in the successful case or returns an error +response in the failure case. + +We'll call this a "high-order handler" similar to the "high-order component" +pattern common in the React ecosystem. + +```ts +// New Platform high-order handler +const checkLicense = ( + handler: RequestHandler +): RequestHandler => { + return (context, req, res) => { + const licenseInfo = getMyPluginLicenseInfo(context.licensing.license); + + if (licenseInfo.hasAtLeast('gold')) { + return handler(context, req, res); + } else { + return res.forbidden({ body: `You don't have the right license for MyPlugin!` }); + } + } +} + +router.get( + { path: '/api/my-plugin/do-something', validate: false }, + checkLicense(async (context, req, res) => { + const results = doSomethingInteresting(); + return res.ok({ body: results }); + }), +) +``` + +#### Full Example + +In some cases, the route handler may need access to data that the pre-handler +retrieves. In this case, you can utilize a handler _factory_ rather than a raw +handler. + +```ts +// Legacy pre-handler +const licensePreRouting = (request) => { + const licenseInfo = getMyPluginLicenseInfo(request.server.plugins.xpack_main); + if (licenseInfo.isOneOf(['gold', 'platinum', 'trial'])) { + // In this case, the return value of the pre-handler is made available on + // whatever the 'assign' option is in the route config. + return licenseInfo; + } else { + // In this case, the route handler is never called and the user gets this + // error message + throw Boom.forbidden(`You don't have the right license for MyPlugin!`); + } +} + +server.route({ + method: 'GET', + path: '/api/my-plugin/do-something', + config: { + pre: [{ method: licensePreRouting, assign: 'licenseInfo' }] + }, + handler: (req) => { + const licenseInfo = req.pre.licenseInfo; + return doSomethingInteresting(licenseInfo); + } +}) +``` + +In many cases, it may be simpler to duplicate the function call +to retrieve the data again in the main handler. In this other cases, you can +utilize a handler _factory_ rather than a raw handler as the argument to your +high-order handler. This way the high-order handler can pass arbitrary arguments +to the route handler. + +```ts +// New Platform high-order handler +const checkLicense = ( + handlerFactory: (licenseInfo: MyPluginLicenseInfo) => RequestHandler +): RequestHandler => { + return (context, req, res) => { + const licenseInfo = getMyPluginLicenseInfo(context.licensing.license); + + if (licenseInfo.hasAtLeast('gold')) { + const handler = handlerFactory(licenseInfo); + return handler(context, req, res); + } else { + return res.forbidden({ body: `You don't have the right license for MyPlugin!` }); + } + } +} + +router.get( + { path: '/api/my-plugin/do-something', validate: false }, + checkLicense(licenseInfo => async (context, req, res) => { + const results = doSomethingInteresting(licenseInfo); + return res.ok({ body: results }); + }), +) +``` + ## Chrome In the Legacy Platform, the `ui/chrome` import contained APIs for a very wide diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 7a82ca0b1609c..34ba25f92beb6 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -18,6 +18,11 @@ kibana_vars=( console.enabled console.proxyConfig console.proxyFilter + cpu.cgroup.path.override + cpuacct.cgroup.path.override + csp.rules + csp.strict + csp.warnLegacyBrowsers elasticsearch.customHeaders elasticsearch.hosts elasticsearch.logQueries @@ -30,6 +35,7 @@ kibana_vars=( elasticsearch.sniffInterval elasticsearch.sniffOnConnectionFault elasticsearch.sniffOnStart + elasticsearch.ssl.alwaysPresentCertificate elasticsearch.ssl.certificate elasticsearch.ssl.certificateAuthorities elasticsearch.ssl.key @@ -42,9 +48,13 @@ kibana_vars=( elasticsearch.startupTimeout elasticsearch.username i18n.locale + interpreter.enableInVisualize + kibana.autocompleteTerminateAfter + kibana.autocompleteTimeout kibana.defaultAppId kibana.index logging.dest + logging.json logging.quiet logging.rotate.enabled logging.rotate.everyBytes @@ -55,18 +65,32 @@ kibana_vars=( logging.useUTC logging.verbose map.includeElasticMapsService + map.proxyElasticMapsServiceInMaps + map.regionmap + map.tilemap.options.attribution + map.tilemap.options.maxZoom + map.tilemap.options.minZoom + map.tilemap.options.subdomains + map.tilemap.url + newsfeed.enabled ops.interval path.data pid.file regionmap server.basePath server.customResponseHeaders + server.compression.enabled + server.compression.referrerWhitelist + server.cors + server.cors.origin server.defaultRoute server.host + server.keepAliveTimeout server.maxPayloadBytes server.name server.port server.rewriteBasePath + server.socketTimeout server.ssl.cert server.ssl.certificate server.ssl.certificateAuthorities @@ -82,6 +106,7 @@ kibana_vars=( server.ssl.truststore.password server.ssl.redirectHttpFromPort server.ssl.supportedProtocols + server.xsrf.disableProtection server.xsrf.whitelist status.allowAnonymous status.v6ApiFormat @@ -96,11 +121,13 @@ kibana_vars=( xpack.apm.serviceMapEnabled xpack.apm.ui.enabled xpack.apm.ui.maxTraceItems + xpack.apm.ui.transactionGroupBucketSize apm_oss.apmAgentConfigurationIndex apm_oss.indexPattern apm_oss.errorIndices apm_oss.onboardingIndices apm_oss.spanIndices + apm_oss.sourcemapIndices apm_oss.transactionIndices apm_oss.metricsIndices xpack.canvas.enabled @@ -116,6 +143,8 @@ kibana_vars=( xpack.code.security.gitHostWhitelist xpack.code.security.gitProtocolWhitelist xpack.graph.enabled + xpack.graph.canEditDrillDownUrls + xpack.graph.savePolicy xpack.grokdebugger.enabled xpack.infra.enabled xpack.infra.query.partitionFactor @@ -128,11 +157,14 @@ kibana_vars=( xpack.infra.sources.default.fields.timestamp xpack.infra.sources.default.logAlias xpack.infra.sources.default.metricAlias + xpack.license_management.enabled xpack.ml.enabled + xpack.monitoring.cluster_alerts.email_notifications.email_address xpack.monitoring.elasticsearch.password xpack.monitoring.elasticsearch.pingTimeout xpack.monitoring.elasticsearch.hosts xpack.monitoring.elasticsearch.username + xpack.monitoring.elasticsearch.logFetchCount xpack.monitoring.elasticsearch.ssl.certificateAuthorities xpack.monitoring.elasticsearch.ssl.verificationMode xpack.monitoring.enabled @@ -166,6 +198,7 @@ kibana_vars=( xpack.reporting.csv.maxSizeBytes xpack.reporting.csv.scroll.duration xpack.reporting.csv.scroll.size + xpack.reporting.capture.maxAttempts xpack.reporting.enabled xpack.reporting.encryptionKey xpack.reporting.index @@ -183,6 +216,8 @@ kibana_vars=( xpack.reporting.queue.pollIntervalErrorMultiplier xpack.reporting.queue.timeout xpack.reporting.roles.allow + xpack.rollup.enabled + xpack.security.audit.enabled xpack.searchprofiler.enabled xpack.security.authc.providers xpack.security.authc.oidc.realm @@ -196,7 +231,10 @@ kibana_vars=( xpack.security.session.idleTimeout xpack.security.session.lifespan xpack.security.loginAssistanceMessage + telemetry.allowChangingOptInStatus telemetry.enabled + telemetry.optIn + telemetry.optInStatusUrl telemetry.sendUsageFrom ) diff --git a/src/plugins/data/common/es_query/kuery/types.ts b/src/plugins/data/common/es_query/kuery/types.ts index 86cb7e08a767c..63c52bb64dc65 100644 --- a/src/plugins/data/common/es_query/kuery/types.ts +++ b/src/plugins/data/common/es_query/kuery/types.ts @@ -33,6 +33,8 @@ export interface KueryParseOptions { startRule: string; allowLeadingWildcards: boolean; errorOnLuceneSyntax: boolean; + cursorSymbol?: string; + parseCursor?: boolean; } export { nodeTypes } from './node_types'; diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index 0527f833b0f8c..78bd2ec85f477 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -75,3 +75,9 @@ export class AutocompleteService { this.querySuggestionProviders.clear(); } } + +/** @public **/ +export type AutocompleteSetup = ReturnType; + +/** @public **/ +export type AutocompleteStart = ReturnType; diff --git a/src/plugins/data/public/autocomplete/index.ts b/src/plugins/data/public/autocomplete/index.ts index 5b8f3ae510bfd..c2b21e84b7a38 100644 --- a/src/plugins/data/public/autocomplete/index.ts +++ b/src/plugins/data/public/autocomplete/index.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import * as autocomplete from './static'; +export { AutocompleteService, AutocompleteSetup, AutocompleteStart } from './autocomplete_service'; -export { AutocompleteService } from './autocomplete_service'; -export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types'; +export { autocomplete }; diff --git a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts index 53abdd44c0c3f..94054ed56f42a 100644 --- a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts @@ -19,13 +19,20 @@ import { IFieldType, IIndexPattern } from '../../../common/index_patterns'; -export type QuerySuggestionType = 'field' | 'value' | 'operator' | 'conjunction' | 'recentSearch'; +export enum QuerySuggestionsTypes { + Field = 'field', + Value = 'value', + Operator = 'operator', + Conjunction = 'conjunction', + RecentSearch = 'recentSearch', +} export type QuerySuggestionsGetFn = ( args: QuerySuggestionsGetFnArgs ) => Promise | undefined; -interface QuerySuggestionsGetFnArgs { +/** @public **/ +export interface QuerySuggestionsGetFnArgs { language: string; indexPatterns: IIndexPattern[]; query: string; @@ -35,22 +42,21 @@ interface QuerySuggestionsGetFnArgs { boolFilter?: any; } -interface BasicQuerySuggestion { - type: QuerySuggestionType; - description?: string; +/** @public **/ +export interface BasicQuerySuggestion { + type: QuerySuggestionsTypes; + description?: string | JSX.Element; end: number; start: number; text: string; cursorIndex?: number; } -interface FieldQuerySuggestion extends BasicQuerySuggestion { - type: 'field'; +/** @public **/ +export interface FieldQuerySuggestion extends BasicQuerySuggestion { + type: QuerySuggestionsTypes.Field; field: IFieldType; } -// A union type allows us to do easy type guards in the code. For example, if I want to ensure I'm -// working with a FieldAutocompleteSuggestion, I can just do `if ('field' in suggestion)` and the -// TypeScript compiler will narrow the type to the parts of the union that have a field prop. /** @public **/ export type QuerySuggestion = BasicQuerySuggestion | FieldQuerySuggestion; diff --git a/src/plugins/data/public/autocomplete/types.ts b/src/plugins/data/public/autocomplete/static.ts similarity index 76% rename from src/plugins/data/public/autocomplete/types.ts rename to src/plugins/data/public/autocomplete/static.ts index 759e2dd25a5bc..7d627486c6d65 100644 --- a/src/plugins/data/public/autocomplete/types.ts +++ b/src/plugins/data/public/autocomplete/static.ts @@ -17,17 +17,11 @@ * under the License. */ -import { AutocompleteService } from './autocomplete_service'; - -/** @public **/ -export type AutocompleteSetup = ReturnType; - -/** @public **/ -export type AutocompleteStart = ReturnType; - -/** @public **/ export { QuerySuggestion, + QuerySuggestionsTypes, QuerySuggestionsGetFn, - QuerySuggestionType, + QuerySuggestionsGetFnArgs, + BasicQuerySuggestion, + FieldQuerySuggestion, } from './providers/query_suggestion_provider'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index bc25c64f0e96e..2fa6b8deae69d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -18,7 +18,6 @@ */ import { PluginInitializerContext } from '../../../core/public'; -import * as autocomplete from './autocomplete'; export function plugin(initializerContext: PluginInitializerContext) { return new DataPublicPlugin(initializerContext); @@ -44,7 +43,7 @@ export { RefreshInterval, TimeRange, } from '../common'; - +export { autocomplete } from './autocomplete'; export * from './field_formats'; export * from './index_patterns'; export * from './search'; @@ -70,5 +69,3 @@ export { // Export plugin after all other imports import { DataPublicPlugin } from './plugin'; export { DataPublicPlugin as Plugin }; - -export { autocomplete }; diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts index 1ade48b3537c8..8c790ac2ddc46 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts @@ -29,9 +29,16 @@ import { esFilters } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; const setupMock = coreMock.createSetup(); -setupMock.uiSettings.get.mockImplementation((key: string) => { - return true; -}); +const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } +}; + +setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); describe('filter_manager', () => { let updateSubscription: Subscription | undefined; @@ -224,6 +231,44 @@ describe('filter_manager', () => { expect(newGlobalFilters).toHaveLength(2); expect(newAppFilters).toHaveLength(1); }); + + test('set filter with no state, and force pin', async () => { + const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38); + f1.$state = undefined; + + filterManager.setFilters([f1], true); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(filterManager.getAppFilters()).toHaveLength(0); + }); + + test('set filter with no state, and no pin', async () => { + const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38); + f1.$state = undefined; + + filterManager.setFilters([f1], false); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(filterManager.getAppFilters()).toHaveLength(1); + }); + + test('set filters with default pin', async () => { + const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38); + f1.$state = undefined; + setupMock.uiSettings.get.mockImplementationOnce(uiSettingsMock(true)); + + filterManager.setFilters([f1]); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(filterManager.getAppFilters()).toHaveLength(0); + }); + + test('set filters without default pin', async () => { + const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 38); + f1.$state = undefined; + + setupMock.uiSettings.get.mockImplementationOnce(uiSettingsMock(false)); + filterManager.setFilters([f1]); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(filterManager.getAppFilters()).toHaveLength(1); + }); }); describe('add filters', () => { diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index 6c5cdbaffce5e..aa77f10d89f63 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -124,7 +124,10 @@ export class FilterManager { /* Setters */ - public addFilters(filters: esFilters.Filter[] | esFilters.Filter, pinFilterStatus?: boolean) { + public addFilters( + filters: esFilters.Filter[] | esFilters.Filter, + pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault') + ) { if (!Array.isArray(filters)) { filters = [filters]; } @@ -133,12 +136,6 @@ export class FilterManager { return; } - if (pinFilterStatus === undefined) { - pinFilterStatus = this.uiSettings.get('filters:pinnedByDefault'); - } - - // Set the store of all filters. For now. - // In the future, all filters should come in with filter state store already set. const store = pinFilterStatus ? esFilters.FilterStateStore.GLOBAL_STATE : esFilters.FilterStateStore.APP_STATE; @@ -157,7 +154,16 @@ export class FilterManager { this.handleStateUpdate(newFilters); } - public setFilters(newFilters: esFilters.Filter[]) { + public setFilters( + newFilters: esFilters.Filter[], + pinFilterStatus: boolean = this.uiSettings.get('filters:pinnedByDefault') + ) { + const store = pinFilterStatus + ? esFilters.FilterStateStore.GLOBAL_STATE + : esFilters.FilterStateStore.APP_STATE; + + FilterManager.setFiltersStore(newFilters, store); + const mappedFilters = mapAndFlattenFilters(newFilters); const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters); const mergedFilters = this.mergeIncomingFilters(newPartitionedFilters); diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 6b6ff5e62e63f..e62aba5f2713d 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -20,7 +20,7 @@ import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public'; -import { AutocompleteSetup, AutocompleteStart } from './autocomplete/types'; +import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; import { ISearchSetup, ISearchStart } from './search'; import { QuerySetup, QueryStart } from './query'; diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index cf219c35bcced..7a8c0f7269fa1 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -89,8 +89,6 @@ const KEY_CODES = { END: 35, }; -const recentSearchType: autocomplete.QuerySuggestionType = 'recentSearch'; - export class QueryStringInputUI extends Component { public state: State = { isSuggestionsVisible: false, @@ -193,7 +191,7 @@ export class QueryStringInputUI extends Component { const text = toUser(recentSearch); const start = 0; const end = query.length; - return { type: recentSearchType, text, start, end }; + return { type: autocomplete.QuerySuggestionsTypes.RecentSearch, text, start, end }; }); }; @@ -343,7 +341,7 @@ export class QueryStringInputUI extends Component { selectionEnd: start + (cursorIndex ? cursorIndex : text.length), }); - if (type === recentSearchType) { + if (type === autocomplete.QuerySuggestionsTypes.RecentSearch) { this.setState({ isSuggestionsVisible: false, index: null }); this.onSubmit({ query: newQueryString, language: this.props.query.language }); } diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx index 0c5c701642757..ba92be8947ea5 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -31,7 +31,7 @@ const mockSuggestion: autocomplete.QuerySuggestion = { end: 0, start: 42, text: 'as promised, not helpful', - type: 'value', + type: autocomplete.QuerySuggestionsTypes.Value, }; describe('SuggestionComponent', () => { diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index b84f612b6d13a..eebe438025949 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -33,14 +33,14 @@ const mockSuggestions: autocomplete.QuerySuggestion[] = [ end: 0, start: 42, text: 'as promised, not helpful', - type: 'value', + type: autocomplete.QuerySuggestionsTypes.Value, }, { description: 'This is another unhelpful suggestion', end: 0, start: 42, text: 'yep', - type: 'field', + type: autocomplete.QuerySuggestionsTypes.Field, }, ]; 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 5b7ddc2b450d6..07ea97f442b7f 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 @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useState, useEffect } from 'react'; import { EuiButtonIcon, EuiPanel } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; +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')` @@ -58,6 +59,17 @@ export function Controls() { } }, [cy]); + function center() { + if (cy) { + const eles = cy.nodes(); + cy.animate({ + ...animationOptions, + center: { eles }, + fit: { eles, padding: nodeHeight } + }); + } + } + function zoomIn() { doZoom(cy, increment); } @@ -82,6 +94,9 @@ export function Controls() { const zoomOutLabel = i18n.translate('xpack.apm.serviceMap.zoomOut', { defaultMessage: 'Zoom out' }); + const centerLabel = i18n.translate('xpack.apm.serviceMap.center', { + defaultMessage: 'Center' + }); return ( @@ -103,6 +118,15 @@ export function Controls() { title={zoomOutLabel} /> + +