diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 27fb9b63843c4..657104344662f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -441,14 +441,6 @@ function VisualizeAppController( }) ); - subscriptions.add( - subscribeWithScope($scope, timefilter.getAutoRefreshFetch$(), { - next: () => { - $scope.vis.forceReload(); - }, - }) - ); - $scope.$on('$destroy', () => { if ($scope._handler) { $scope._handler.destroy(); diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index 933c249cd7279..0394dea343adf 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -41,8 +41,8 @@ export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; callCluster: CallCluster; server: any; - start: string; - end: string; + start: string | number; + end: string | number; } export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig; @@ -193,7 +193,7 @@ export class TelemetryCollectionManager { } } catch (err) { statsCollectionConfig.server.log( - ['debu', 'telemetry', 'collection'], + ['debug', 'telemetry', 'collection'], `Failed to collect any usage with registered collections.` ); // swallow error to try next collection; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 5e593398333c9..fddcf70c30605 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -34,6 +34,7 @@ import { esFilters, Filter, ISearchSource, + TimefilterContract, } from '../../../../../plugins/data/public'; import { EmbeddableInput, @@ -106,8 +107,10 @@ export class VisualizeEmbeddable extends Embeddable { this.handleChanges(); @@ -345,6 +352,7 @@ export class VisualizeEmbeddable extends Embeddable { diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 03471174753fa..2f00467a85cda 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -38,6 +38,7 @@ import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { getCapabilities, getHttp, getTypes, getUISettings } from '../np_ready/public/services'; import { showNewVisModal } from '../np_ready/public/wizard'; +import { TimefilterContract } from '../../../../../plugins/data/public'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; @@ -51,7 +52,10 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< > { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - constructor(private getSavedVisualizationsLoader: () => SavedVisualizations) { + constructor( + private timefilter: TimefilterContract, + private getSavedVisualizationsLoader: () => SavedVisualizations + ) { super({ savedObjectMetaData: { name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), @@ -114,6 +118,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< const indexPattern = await getIndexPattern(savedObject); const indexPatterns = indexPattern ? [indexPattern] : []; return new VisualizeEmbeddable( + this.timefilter, { savedVisualization: savedObject, indexPatterns, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index a948757d7bd83..9fb87cadb2983 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -56,6 +56,7 @@ const createInstance = async () => { const plugin = new VisualizationsPlugin({} as PluginInitializerContext); const setup = plugin.setup(coreMock.createSetup(), { + data: dataPluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createStartContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 36c04923e3fd0..20bed59faad88 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -36,7 +36,10 @@ import { ExpressionsSetup } from '../../../../../../plugins/expressions/public'; import { IEmbeddableSetup } from '../../../../../../plugins/embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; -import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; import { createSavedVisLoader, @@ -65,6 +68,7 @@ export interface VisualizationsSetupDeps { expressions: ExpressionsSetup; embeddable: IEmbeddableSetup; usageCollection: UsageCollectionSetup; + data: DataPublicPluginSetup; } export interface VisualizationsStartDeps { @@ -95,7 +99,7 @@ export class VisualizationsPlugin public setup( core: CoreSetup, - { expressions, embeddable, usageCollection }: VisualizationsSetupDeps + { expressions, embeddable, usageCollection, data }: VisualizationsSetupDeps ): VisualizationsSetup { setUISettings(core.uiSettings); setUsageCollector(usageCollection); @@ -103,7 +107,10 @@ export class VisualizationsPlugin expressions.registerFunction(visualizationFunction); expressions.registerRenderer(visualizationRenderer); - const embeddableFactory = new VisualizeEmbeddableFactory(this.getSavedVisualizationsLoader); + const embeddableFactory = new VisualizeEmbeddableFactory( + data.query.timefilter.timefilter, + this.getSavedVisualizationsLoader + ); embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory); return { diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx index e232e41902cbb..ca468f85275a8 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -26,7 +26,7 @@ import { useRequestReadContext, } from '../../../../contexts'; -import * as utils from '../../../../../lib/utils/utils'; +import { expandLiteralStrings } from '../../../../../../../es_ui_shared/console_lang/lib'; import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { applyCurrentSettings } from './apply_editor_settings'; @@ -67,7 +67,7 @@ function EditorOutputUI() { editor.update( data .map(d => d.response.value as string) - .map(readOnlySettings.tripleQuotes ? utils.expandLiteralStrings : a => a) + .map(readOnlySettings.tripleQuotes ? expandLiteralStrings : a => a) .join('\n') ); } else if (error) { diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 35d9865ee4ccb..102f90a9feb6f 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -17,10 +17,11 @@ * under the License. */ -import * as utils from '../../../lib/utils/utils'; +import { extractDeprecationMessages } from '../../../lib/utils'; +import { collapseLiteralStrings } from '../../../../../es_ui_shared/console_lang/lib'; // @ts-ignore import * as es from '../../../lib/es/es'; -import { BaseResponseType } from '../../../types/common'; +import { BaseResponseType } from '../../../types'; export interface EsRequestArgs { requests: any; @@ -73,7 +74,7 @@ export function sendRequestToES(args: EsRequestArgs): Promise const req = requests.shift(); const esPath = req.url; const esMethod = req.method; - let esData = utils.collapseLiteralStrings(req.data.join('\n')); + let esData = collapseLiteralStrings(req.data.join('\n')); if (esData) { esData += '\n'; } // append a new line for bulk requests. @@ -97,7 +98,7 @@ export function sendRequestToES(args: EsRequestArgs): Promise const warnings = xhr.getResponseHeader('warning'); if (warnings) { - const deprecationMessages = utils.extractDeprecationMessages(warnings); + const deprecationMessages = extractDeprecationMessages(warnings); value = deprecationMessages.join('\n') + '\n' + value; } diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js index da101b61f8035..d763db7ae5d79 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/input.js @@ -18,6 +18,7 @@ */ import ace from 'brace'; +import { workerModule } from './worker'; const oop = ace.acequire('ace/lib/oop'); const TextMode = ace.acequire('ace/mode/text').Mode; @@ -29,7 +30,6 @@ const WorkerClient = ace.acequire('ace/worker/worker_client').WorkerClient; const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; const HighlightRules = require('./input_highlight_rules').InputHighlightRules; -import { workerModule } from './worker'; export function Mode() { this.$tokenizer = new AceTokenizer(new HighlightRules().getRules()); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js index 842736428e8bb..2c1b30f806f95 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js @@ -18,7 +18,7 @@ */ const ace = require('brace'); -import { addToRules } from './x_json_highlight_rules'; +import { addXJsonToRules } from '../../../../../../es_ui_shared/console_lang'; export function addEOL(tokens, reg, nextIfEOL, normalNext) { if (typeof reg === 'object') { @@ -101,7 +101,7 @@ export function InputHighlightRules() { ), }; - addToRules(this); + addXJsonToRules(this); if (this.constructor === InputHighlightRules) { this.normalizeRules(); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js index 920bdff1798db..e27222ebd65e9 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js @@ -19,7 +19,7 @@ const ace = require('brace'); import 'brace/mode/json'; -import { addToRules } from './x_json_highlight_rules'; +import { addXJsonToRules } from '../../../../../../es_ui_shared/console_lang'; const oop = ace.acequire('ace/lib/oop'); const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules; @@ -27,7 +27,7 @@ const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHig export function OutputJsonHighlightRules() { this.$rules = {}; - addToRules(this, 'start'); + addXJsonToRules(this, 'start'); this.$rules.start.unshift( { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js index b6b74c6377233..13ae329380221 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/script.js @@ -18,17 +18,15 @@ */ import ace from 'brace'; +import { ScriptHighlightRules } from '../../../../../../es_ui_shared/console_lang'; const oop = ace.acequire('ace/lib/oop'); const TextMode = ace.acequire('ace/mode/text').Mode; const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent; const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour; const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; -//const WorkerClient = ace.acequire('ace/worker/worker_client').WorkerClient; ace.acequire('ace/tokenizer'); -const ScriptHighlightRules = require('./script_highlight_rules').ScriptHighlightRules; - export function ScriptMode() { this.$outdent = new MatchingBraceOutdent(); this.$behaviour = new CstyleBehaviour(); @@ -57,19 +55,4 @@ oop.inherits(ScriptMode, TextMode); this.autoOutdent = function(state, doc, row) { this.$outdent.autoOutdent(doc, row); }; - - // this.createWorker = function (session) { - // const worker = new WorkerClient(['ace', 'sense_editor'], 'sense_editor/mode/worker', 'SenseWorker', 'sense_editor/mode/worker'); - // worker.attachToDocument(session.getDocument()); - - // worker.on('error', function (e) { - // session.setAnnotations([e.data]); - // }); - - // worker.on('ok', function (anno) { - // session.setAnnotations(anno.data); - // }); - - // return worker; - // }; }.call(ScriptMode.prototype)); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts new file mode 100644 index 0000000000000..c7ceb6a95b896 --- /dev/null +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.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 declare const workerModule: { id: string; src: string }; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js index a66bc20685df7..63f97345bc9ff 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js @@ -22,8 +22,8 @@ import $ from 'jquery'; import _ from 'lodash'; import { create } from '../create'; +import { collapseLiteralStrings } from '../../../../../../es_ui_shared/console_lang/lib'; const editorInput1 = require('./editor_input1.txt'); -const utils = require('../../../../lib/utils/utils'); describe('Editor', () => { let input; @@ -331,7 +331,7 @@ describe('Editor', () => { const expected = { method: 'POST', url: '_search', - data: [utils.collapseLiteralStrings(simpleRequest.data)], + data: [collapseLiteralStrings(simpleRequest.data)], }; compareRequest(request, expected); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index 1271f167c6cc1..f559f5dfcd707 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -19,7 +19,9 @@ import _ from 'lodash'; import RowParser from '../../../lib/row_parser'; -import * as utils from '../../../lib/utils/utils'; +import { collapseLiteralStrings } from '../../../../../es_ui_shared/console_lang/lib'; +import * as utils from '../../../lib/utils'; + // @ts-ignore import * as es from '../../../lib/es/es'; @@ -480,7 +482,7 @@ export class SenseEditor { let ret = 'curl -X' + esMethod + ' "' + url + '"'; if (esData && esData.length) { ret += " -H 'Content-Type: application/json' -d'\n"; - const dataAsString = utils.collapseLiteralStrings(esData.join('\n')); + const dataAsString = collapseLiteralStrings(esData.join('\n')); // since Sense doesn't allow single quote json string any single qoute is within a string. ret += dataAsString.replace(/'/g, '\\"'); if (esData.length > 1) { diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts index 00bfe32c85906..d5465ebe96514 100644 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts +++ b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import '../../application/models/sense_editor/sense_editor.test.mocks'; import $ from 'jquery'; diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index ac8fa1ea48caa..e09024ccfc859 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -29,7 +29,7 @@ import { // @ts-ignore } from '../kb/kb'; -import * as utils from '../utils/utils'; +import * as utils from '../utils'; // @ts-ignore import { populateContext } from './engine'; diff --git a/src/plugins/console/public/lib/utils/__tests__/utils.test.js b/src/plugins/console/public/lib/utils/__tests__/utils.test.js index f6828f354a1bc..6115be3c84ed9 100644 --- a/src/plugins/console/public/lib/utils/__tests__/utils.test.js +++ b/src/plugins/console/public/lib/utils/__tests__/utils.test.js @@ -17,125 +17,80 @@ * under the License. */ -const _ = require('lodash'); -const utils = require('../utils'); -const collapsingTests = require('./utils_string_collapsing.txt'); -const expandingTests = require('./utils_string_expanding.txt'); +const utils = require('../'); describe('Utils class', () => { - describe('collapseLiteralStrings', () => { - it('will collapse multiline strings', () => { - const multiline = '{ "foo": """bar\nbaz""" }'; - expect(utils.collapseLiteralStrings(multiline)).toEqual('{ "foo": "bar\\nbaz" }'); - }); + test('extract deprecation messages', function() { + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT"' + ) + ).toEqual(['#! Deprecation: this is a warning']); + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning"' + ) + ).toEqual(['#! Deprecation: this is a warning']); - it('will collapse multiline strings with CRLF endings', () => { - const multiline = '{ "foo": """bar\r\nbaz""" }'; - expect(utils.collapseLiteralStrings(multiline)).toEqual('{ "foo": "bar\\r\\nbaz" }'); - }); - }); + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning" "Mon, 27 Feb 2017 14:52:14 GMT"' + ) + ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning"' + ) + ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); - _.each(collapsingTests.split(/^=+$/m), function(fixture) { - if (fixture.trim() === '') { - return; - } - fixture = fixture.split(/^-+$/m); - const name = fixture[0].trim(); - const expanded = fixture[1].trim(); - const collapsed = fixture[2].trim(); + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma" "Mon, 27 Feb 2017 14:52:14 GMT"' + ) + ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma"' + ) + ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); - test('Literal collapse - ' + name, function() { - expect(utils.collapseLiteralStrings(expanded)).toEqual(collapsed); - }); + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\"" "Mon, 27 Feb 2017 14:52:14 GMT"' + ) + ).toEqual([ + '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', + ]); + expect( + utils.extractDeprecationMessages( + '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\""' + ) + ).toEqual([ + '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', + ]); }); - _.each(expandingTests.split(/^=+$/m), function(fixture) { - if (fixture.trim() === '') { - return; - } - fixture = fixture.split(/^-+$/m); - const name = fixture[0].trim(); - const collapsed = fixture[1].trim(); - const expanded = fixture[2].trim(); - - test('Literal expand - ' + name, function() { - expect(utils.expandLiteralStrings(collapsed)).toEqual(expanded); - }); - - test('extract deprecation messages', function() { - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT"' - ) - ).toEqual(['#! Deprecation: this is a warning']); - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning"' - ) - ).toEqual(['#! Deprecation: this is a warning']); - - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning" "Mon, 27 Feb 2017 14:52:14 GMT"' - ) - ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning"' - ) - ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); - - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma" "Mon, 27 Feb 2017 14:52:14 GMT"' - ) - ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma"' - ) - ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); - - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\"" "Mon, 27 Feb 2017 14:52:14 GMT"' - ) - ).toEqual([ - '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', - ]); - expect( - utils.extractDeprecationMessages( - '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\""' - ) - ).toEqual([ - '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', - ]); - }); - - test('unescape', function() { - expect(utils.unescape('escaped backslash \\\\')).toEqual('escaped backslash \\'); - expect(utils.unescape('a pair of \\"escaped quotes\\"')).toEqual( - 'a pair of "escaped quotes"' - ); - expect(utils.unescape('escaped quotes do not have to come in pairs: \\"')).toEqual( - 'escaped quotes do not have to come in pairs: "' - ); - }); + test('unescape', function() { + expect(utils.unescape('escaped backslash \\\\')).toEqual('escaped backslash \\'); + expect(utils.unescape('a pair of \\"escaped quotes\\"')).toEqual('a pair of "escaped quotes"'); + expect(utils.unescape('escaped quotes do not have to come in pairs: \\"')).toEqual( + 'escaped quotes do not have to come in pairs: "' + ); + }); - test('split on unquoted comma followed by space', function() { - expect(utils.splitOnUnquotedCommaSpace('a, b')).toEqual(['a', 'b']); - expect(utils.splitOnUnquotedCommaSpace('a,b, c')).toEqual(['a,b', 'c']); - expect(utils.splitOnUnquotedCommaSpace('"a, b"')).toEqual(['"a, b"']); - expect(utils.splitOnUnquotedCommaSpace('"a, b", c')).toEqual(['"a, b"', 'c']); - expect(utils.splitOnUnquotedCommaSpace('"a, b\\", c"')).toEqual(['"a, b\\", c"']); - expect(utils.splitOnUnquotedCommaSpace(', a, b')).toEqual(['', 'a', 'b']); - expect(utils.splitOnUnquotedCommaSpace('a, b, ')).toEqual(['a', 'b', '']); - expect(utils.splitOnUnquotedCommaSpace('\\"a, b", "c, d\\", e", f"')).toEqual([ - '\\"a', - 'b", "c', - 'd\\"', - 'e", f"', - ]); - }); + test('split on unquoted comma followed by space', function() { + expect(utils.splitOnUnquotedCommaSpace('a, b')).toEqual(['a', 'b']); + expect(utils.splitOnUnquotedCommaSpace('a,b, c')).toEqual(['a,b', 'c']); + expect(utils.splitOnUnquotedCommaSpace('"a, b"')).toEqual(['"a, b"']); + expect(utils.splitOnUnquotedCommaSpace('"a, b", c')).toEqual(['"a, b"', 'c']); + expect(utils.splitOnUnquotedCommaSpace('"a, b\\", c"')).toEqual(['"a, b\\", c"']); + expect(utils.splitOnUnquotedCommaSpace(', a, b')).toEqual(['', 'a', 'b']); + expect(utils.splitOnUnquotedCommaSpace('a, b, ')).toEqual(['a', 'b', '']); + expect(utils.splitOnUnquotedCommaSpace('\\"a, b", "c, d\\", e", f"')).toEqual([ + '\\"a', + 'b", "c', + 'd\\"', + 'e", f"', + ]); }); }); diff --git a/src/plugins/console/public/lib/utils/utils.ts b/src/plugins/console/public/lib/utils/index.ts similarity index 59% rename from src/plugins/console/public/lib/utils/utils.ts rename to src/plugins/console/public/lib/utils/index.ts index 0b10938abe704..f66c952dd3af7 100644 --- a/src/plugins/console/public/lib/utils/utils.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -18,6 +18,10 @@ */ import _ from 'lodash'; +import { + expandLiteralStrings, + collapseLiteralStrings, +} from '../../../../es_ui_shared/console_lang/lib'; export function textFromRequest(request: any) { let data = request.data; @@ -56,59 +60,6 @@ export function formatRequestBodyDoc(data: string[], indent: boolean) { }; } -export function collapseLiteralStrings(data: any) { - const splitData = data.split(`"""`); - for (let idx = 1; idx < splitData.length - 1; idx += 2) { - splitData[idx] = JSON.stringify(splitData[idx]); - } - return splitData.join(''); -} - -/* - The following regex describes global match on: - 1. one colon followed by any number of space characters - 2. one double quote (not escaped, special case for JSON in JSON). - 3. greedily match any non double quote and non newline char OR any escaped double quote char (non-capturing). - 4. handle a special case where an escaped slash may be the last character - 5. one double quote - - For instance: `: "some characters \" here"` - Will match and be expanded to: `"""some characters " here"""` - - */ - -const LITERAL_STRING_CANDIDATES = /((:[\s\r\n]*)([^\\])"(\\"|[^"\n])*\\?")/g; - -export function expandLiteralStrings(data: string) { - return data.replace(LITERAL_STRING_CANDIDATES, (match, string) => { - // Expand to triple quotes if there are _any_ slashes - if (string.match(/\\./)) { - const firstDoubleQuoteIdx = string.indexOf('"'); - const lastDoubleQuoteIdx = string.lastIndexOf('"'); - - // Handle a special case where we may have a value like "\"test\"". We don't - // want to expand this to """"test"""" - so we terminate before processing the string - // further if we detect this either at the start or end of the double quote section. - - if (string[firstDoubleQuoteIdx + 1] === '\\' && string[firstDoubleQuoteIdx + 2] === '"') { - return string; - } - - if (string[lastDoubleQuoteIdx - 1] === '"' && string[lastDoubleQuoteIdx - 2] === '\\') { - return string; - } - - const colonAndAnySpacing = string.slice(0, firstDoubleQuoteIdx); - const rawStringifiedValue = string.slice(firstDoubleQuoteIdx, string.length); - // Remove one level of JSON stringification - const jsonValue = JSON.parse(rawStringifiedValue); - return `${colonAndAnySpacing}"""${jsonValue}"""`; - } else { - return string; - } - }); -} - export function extractDeprecationMessages(warnings: string) { // pattern for valid warning header const re = /\d{3} [0-9a-zA-Z!#$%&'*+-.^_`|~]+ \"((?:\t| |!|[\x23-\x5b]|[\x5d-\x7e]|[\x80-\xff]|\\\\|\\")*)\"(?: \"[^"]*\")?/; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 2923cee60f898..80c13464ad98a 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -18,6 +18,7 @@ */ import { TimefilterService, TimeHistoryContract, TimefilterContract } from '.'; +import { Observable } from 'rxjs'; export type TimefilterServiceClientContract = PublicMethodsOf; @@ -28,7 +29,7 @@ const createSetupContractMock = () => { getEnabledUpdated$: jest.fn(), getTimeUpdate$: jest.fn(), getRefreshIntervalUpdate$: jest.fn(), - getAutoRefreshFetch$: jest.fn(), + getAutoRefreshFetch$: jest.fn(() => new Observable()), getFetch$: jest.fn(), getTime: jest.fn(), setTime: jest.fn(), diff --git a/src/plugins/es_ui_shared/console_lang/ace/modes/index.d.ts b/src/plugins/es_ui_shared/console_lang/ace/modes/index.d.ts new file mode 100644 index 0000000000000..06c9f9a51ea68 --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/ace/modes/index.d.ts @@ -0,0 +1,32 @@ +/* + * 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 { Editor } from 'brace'; + +export declare const ElasticsearchSqlHighlightRules: FunctionConstructor; +export declare const ScriptHighlightRules: FunctionConstructor; +export declare const XJsonHighlightRules: FunctionConstructor; + +export declare const XJsonMode: FunctionConstructor; + +/** + * @param otherRules Another Ace ruleset + * @param embedUnder The state name under which the rules will be embedded. Defaults to "json". + */ +export declare const addXJsonToRules: (otherRules: any, embedUnder?: string) => void; diff --git a/src/plugins/es_ui_shared/console_lang/ace/modes/index.js b/src/plugins/es_ui_shared/console_lang/ace/modes/index.js new file mode 100644 index 0000000000000..955f23116f659 --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/ace/modes/index.js @@ -0,0 +1,27 @@ +/* + * 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 { + ElasticsearchSqlHighlightRules, + ScriptHighlightRules, + XJsonHighlightRules, + addXJsonToRules, +} from './lexer_rules'; + +export { XJsonMode, installXJsonMode } from './x_json'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/elasticsearch_sql_highlight_rules.ts b/src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts similarity index 100% rename from src/plugins/console/public/application/models/legacy_core_editor/mode/elasticsearch_sql_highlight_rules.ts rename to src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts diff --git a/src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/index.js b/src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/index.js new file mode 100644 index 0000000000000..be11fd726b7f2 --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/index.js @@ -0,0 +1,22 @@ +/* + * 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 { ElasticsearchSqlHighlightRules } from './elasticsearch_sql_highlight_rules'; +export { ScriptHighlightRules } from './script_highlight_rules'; +export { XJsonHighlightRules, addToRules as addXJsonToRules } from './x_json_highlight_rules'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/script_highlight_rules.js b/src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/script_highlight_rules.js similarity index 100% rename from src/plugins/console/public/application/models/legacy_core_editor/mode/script_highlight_rules.js rename to src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/script_highlight_rules.js diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/x_json_highlight_rules.js b/src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/x_json_highlight_rules.js similarity index 85% rename from src/plugins/console/public/application/models/legacy_core_editor/mode/x_json_highlight_rules.js rename to src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/x_json_highlight_rules.js index d0a79e84e809d..14323b9320330 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/x_json_highlight_rules.js +++ b/src/plugins/es_ui_shared/console_lang/ace/modes/lexer_rules/x_json_highlight_rules.js @@ -17,11 +17,16 @@ * under the License. */ -const _ = require('lodash'); +import * as _ from 'lodash'; +import ace from 'brace'; +import 'brace/mode/json'; import { ElasticsearchSqlHighlightRules } from './elasticsearch_sql_highlight_rules'; const { ScriptHighlightRules } = require('./script_highlight_rules'); +const { JsonHighlightRules } = ace.acequire('ace/mode/json_highlight_rules'); +const oop = ace.acequire('ace/lib/oop'); + const jsonRules = function(root) { root = root ? root : 'json'; const rules = {}; @@ -146,6 +151,30 @@ const jsonRules = function(root) { return rules; }; +export function XJsonHighlightRules() { + this.$rules = { + ...jsonRules('start'), + }; + + this.embedRules(ScriptHighlightRules, 'script-', [ + { + token: 'punctuation.end_triple_quote', + regex: '"""', + next: 'pop', + }, + ]); + + this.embedRules(ElasticsearchSqlHighlightRules, 'sql-', [ + { + token: 'punctuation.end_triple_quote', + regex: '"""', + next: 'pop', + }, + ]); +} + +oop.inherits(XJsonHighlightRules, JsonHighlightRules); + export function addToRules(otherRules, embedUnder) { otherRules.$rules = _.defaultsDeep(otherRules.$rules, jsonRules(embedUnder)); otherRules.embedRules(ScriptHighlightRules, 'script-', [ diff --git a/src/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts b/src/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts new file mode 100644 index 0000000000000..caa7b518b8b66 --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/ace/modes/x_json/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 { XJsonMode } from './x_json_mode'; diff --git a/src/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json_mode.ts b/src/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json_mode.ts new file mode 100644 index 0000000000000..9f804c29a5d27 --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json_mode.ts @@ -0,0 +1,40 @@ +/* + * 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 ace from 'brace'; + +import { XJsonHighlightRules } from '../index'; + +const oop = ace.acequire('ace/lib/oop'); +const { Mode: JSONMode } = ace.acequire('ace/mode/json'); +const { Tokenizer: AceTokenizer } = ace.acequire('ace/tokenizer'); +const { MatchingBraceOutdent } = ace.acequire('ace/mode/matching_brace_outdent'); +const { CstyleBehaviour } = ace.acequire('ace/mode/behaviour/cstyle'); +const { FoldMode: CStyleFoldMode } = ace.acequire('ace/mode/folding/cstyle'); + +export function XJsonMode(this: any) { + const ruleset: any = new XJsonHighlightRules(); + ruleset.normalizeRules(); + this.$tokenizer = new AceTokenizer(ruleset.getRules()); + this.$outdent = new MatchingBraceOutdent(); + this.$behaviour = new CstyleBehaviour(); + this.foldingRules = new CStyleFoldMode(); +} + +oop.inherits(XJsonMode, JSONMode); diff --git a/src/plugins/es_ui_shared/console_lang/index.ts b/src/plugins/es_ui_shared/console_lang/index.ts new file mode 100644 index 0000000000000..a4958911af412 --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/index.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +// Lib is intentionally not included in this barrel export file to separate worker logic +// from being imported with pure functions + +export { + ElasticsearchSqlHighlightRules, + ScriptHighlightRules, + XJsonHighlightRules, + addXJsonToRules, + XJsonMode, +} from './ace/modes'; diff --git a/src/plugins/es_ui_shared/console_lang/lib/index.ts b/src/plugins/es_ui_shared/console_lang/lib/index.ts new file mode 100644 index 0000000000000..bf7f0290d4158 --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/lib/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 { collapseLiteralStrings, expandLiteralStrings } from './json_xjson_translation_tools'; diff --git a/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts b/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts new file mode 100644 index 0000000000000..92c14ade791cd --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/__tests__/json_xjson_translation_tools.test.ts @@ -0,0 +1,65 @@ +/* + * 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 _ from 'lodash'; +// @ts-ignore +import collapsingTests from './utils_string_collapsing.txt'; +// @ts-ignore +import expandingTests from './utils_string_expanding.txt'; + +import * as utils from '../index'; + +describe('JSON to XJSON conversion tools', () => { + it('will collapse multiline strings', () => { + const multiline = '{ "foo": """bar\nbaz""" }'; + expect(utils.collapseLiteralStrings(multiline)).toEqual('{ "foo": "bar\\nbaz" }'); + }); + + it('will collapse multiline strings with CRLF endings', () => { + const multiline = '{ "foo": """bar\r\nbaz""" }'; + expect(utils.collapseLiteralStrings(multiline)).toEqual('{ "foo": "bar\\r\\nbaz" }'); + }); +}); + +_.each(collapsingTests.split(/^=+$/m), function(fixture) { + if (fixture.trim() === '') { + return; + } + fixture = fixture.split(/^-+$/m); + const name = fixture[0].trim(); + const expanded = fixture[1].trim(); + const collapsed = fixture[2].trim(); + + test('Literal collapse - ' + name, function() { + expect(utils.collapseLiteralStrings(expanded)).toEqual(collapsed); + }); +}); + +_.each(expandingTests.split(/^=+$/m), function(fixture) { + if (fixture.trim() === '') { + return; + } + fixture = fixture.split(/^-+$/m); + const name = fixture[0].trim(); + const collapsed = fixture[1].trim(); + const expanded = fixture[2].trim(); + + test('Literal expand - ' + name, function() { + expect(utils.expandLiteralStrings(collapsed)).toEqual(expanded); + }); +}); diff --git a/src/plugins/console/public/lib/utils/__tests__/utils_string_collapsing.txt b/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_collapsing.txt similarity index 100% rename from src/plugins/console/public/lib/utils/__tests__/utils_string_collapsing.txt rename to src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_collapsing.txt diff --git a/src/plugins/console/public/lib/utils/__tests__/utils_string_expanding.txt b/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_expanding.txt similarity index 100% rename from src/plugins/console/public/lib/utils/__tests__/utils_string_expanding.txt rename to src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/__tests__/utils_string_expanding.txt diff --git a/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/index.ts b/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/index.ts new file mode 100644 index 0000000000000..28f1aca95efab --- /dev/null +++ b/src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools/index.ts @@ -0,0 +1,71 @@ +/* + * 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 function collapseLiteralStrings(data: string) { + const splitData = data.split(`"""`); + for (let idx = 1; idx < splitData.length - 1; idx += 2) { + splitData[idx] = JSON.stringify(splitData[idx]); + } + return splitData.join(''); +} + +/* + The following regex describes global match on: + 1. one colon followed by any number of space characters + 2. one double quote (not escaped, special case for JSON in JSON). + 3. greedily match any non double quote and non newline char OR any escaped double quote char (non-capturing). + 4. handle a special case where an escaped slash may be the last character + 5. one double quote + + For instance: `: "some characters \" here"` + Will match and be expanded to: `"""some characters " here"""` + + */ + +const LITERAL_STRING_CANDIDATES = /((:[\s\r\n]*)([^\\])"(\\"|[^"\n])*\\?")/g; + +export function expandLiteralStrings(data: string) { + return data.replace(LITERAL_STRING_CANDIDATES, (match, string) => { + // Expand to triple quotes if there are _any_ slashes + if (string.match(/\\./)) { + const firstDoubleQuoteIdx = string.indexOf('"'); + const lastDoubleQuoteIdx = string.lastIndexOf('"'); + + // Handle a special case where we may have a value like "\"test\"". We don't + // want to expand this to """"test"""" - so we terminate before processing the string + // further if we detect this either at the start or end of the double quote section. + + if (string[firstDoubleQuoteIdx + 1] === '\\' && string[firstDoubleQuoteIdx + 2] === '"') { + return string; + } + + if (string[lastDoubleQuoteIdx - 1] === '"' && string[lastDoubleQuoteIdx - 2] === '\\') { + return string; + } + + const colonAndAnySpacing = string.slice(0, firstDoubleQuoteIdx); + const rawStringifiedValue = string.slice(firstDoubleQuoteIdx, string.length); + // Remove one level of JSON stringification + const jsonValue = JSON.parse(rawStringifiedValue); + return `${colonAndAnySpacing}"""${jsonValue}"""`; + } else { + return string; + } + }); +} diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index a12c951ad13a8..67e3617a85115 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './components/json_editor'; +export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor'; diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index e102dc2a64ee8..91951aa2f3edf 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -85,7 +85,7 @@ export class Collector { protected defaultFormatterForBulkUpload(result: T) { return { type: this.type, - payload: result, + payload: (result as unknown) as U, }; } } diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts index 05c701bd3abf4..bf861a94fccff 100644 --- a/src/plugins/usage_collection/server/collector/usage_collector.ts +++ b/src/plugins/usage_collection/server/collector/usage_collector.ts @@ -24,14 +24,14 @@ export class UsageCollector ex T, U > { - protected defaultUsageFormatterForBulkUpload(result: T) { + protected defaultFormatterForBulkUpload(result: T) { return { type: KIBANA_STATS_TYPE, - payload: { + payload: ({ usage: { [this.type]: result, }, - }, + } as unknown) as U, }; } } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 51d8b43dac0ea..b58a450d26644 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -5,6 +5,7 @@ */ import { EuiLink } from '@elastic/eui'; +import { Location } from 'history'; import React from 'react'; import url from 'url'; import rison, { RisonValue } from 'rison-node'; @@ -12,6 +13,7 @@ import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData } from '../rison_helpers'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; interface Props { query: { @@ -30,10 +32,15 @@ interface Props { children: React.ReactNode; } -export function DiscoverLink({ query = {}, ...rest }: Props) { - const { core } = useApmPluginContext(); - const location = useLocation(); - +export const getDiscoverHref = ({ + basePath, + location, + query +}: { + basePath: AppMountContextBasePath; + location: Location; + query: Props['query']; +}) => { const risonQuery = { _g: getTimepickerRisonData(location.search), _a: { @@ -43,11 +50,23 @@ export function DiscoverLink({ query = {}, ...rest }: Props) { }; const href = url.format({ - pathname: core.http.basePath.prepend('/app/kibana'), + pathname: basePath.prepend('/app/kibana'), hash: `/discover?_g=${rison.encode(risonQuery._g)}&_a=${rison.encode( risonQuery._a as RisonValue )}` }); + return href; +}; + +export function DiscoverLink({ query = {}, ...rest }: Props) { + const { core } = useApmPluginContext(); + const location = useLocation(); + + const href = getDiscoverHref({ + basePath: core.http.basePath, + query, + location + }); return ; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx index 6d19fca5a819c..f85c5d582329a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -9,12 +9,14 @@ import React from 'react'; import url from 'url'; import { fromQuery } from './url_helpers'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { AppMountContextBasePath } from '../../../context/ApmPluginContext'; import { InfraAppId } from '../../../../../../../plugins/infra/public'; interface InfraQueryParams { time?: number; from?: number; to?: number; + filter?: string; } interface Props extends EuiLinkAnchorProps { @@ -24,12 +26,26 @@ interface Props extends EuiLinkAnchorProps { children?: React.ReactNode; } -export function InfraLink({ app, path, query = {}, ...rest }: Props) { - const { core } = useApmPluginContext(); +export const getInfraHref = ({ + app, + basePath, + query, + path +}: { + app: InfraAppId; + basePath: AppMountContextBasePath; + query: InfraQueryParams; + path?: string; +}) => { const nextSearch = fromQuery(query); - const href = url.format({ - pathname: core.http.basePath.prepend(`/app/${app}${path}`), + return url.format({ + pathname: basePath.prepend(`/app/${app}${path}`), search: nextSearch }); +}; + +export function InfraLink({ app, path, query = {}, ...rest }: Props) { + const { core } = useApmPluginContext(); + const href = getInfraHref({ app, basePath: core.http.basePath, query, path }); return ; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index f990098ae8458..99f0b0d4fc223 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -4,249 +4,85 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPopover, - EuiLink -} from '@elastic/eui'; -import url from 'url'; +import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useState, FunctionComponent } from 'react'; -import { pick } from 'lodash'; +import React, { FunctionComponent, useState } from 'react'; +import { + ActionMenu, + ActionMenuDivider, + Section, + SectionLink, + SectionLinks, + SectionSubtitle, + SectionTitle +} from '../../../../../../../plugins/observability/public'; import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; -import { DiscoverTransactionLink } from '../Links/DiscoverLinks/DiscoverTransactionLink'; -import { InfraLink } from '../Links/InfraLink'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { fromQuery } from '../Links/url_helpers'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { InfraAppId } from '../../../../../../../plugins/infra/public'; - -function getInfraMetricsQuery(transaction: Transaction) { - const plus5 = new Date(transaction['@timestamp']); - const minus5 = new Date(plus5.getTime()); - - plus5.setMinutes(plus5.getMinutes() + 5); - minus5.setMinutes(minus5.getMinutes() - 5); - - return { - from: minus5.getTime(), - to: plus5.getTime() - }; -} - -function ActionMenuButton({ onClick }: { onClick: () => void }) { - return ( - - {i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', { - defaultMessage: 'Actions' - })} - - ); -} +import { useLocation } from '../../../hooks/useLocation'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { getSections } from './sections'; interface Props { readonly transaction: Transaction; } -interface InfraConfigItem { - icon: string; - label: string; - condition?: boolean; - app: InfraAppId; - path: string; - query: Record; -} - -export const TransactionActionMenu: FunctionComponent = ( - props: Props -) => { - const { transaction } = props; - +const ActionMenuButton = ({ onClick }: { onClick: () => void }) => ( + + {i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', { + defaultMessage: 'Actions' + })} + +); + +export const TransactionActionMenu: FunctionComponent = ({ + transaction +}: Props) => { const { core } = useApmPluginContext(); - - const [isOpen, setIsOpen] = useState(false); - + const location = useLocation(); const { urlParams } = useUrlParams(); - const hostName = transaction.host?.hostname; - const podId = transaction.kubernetes?.pod.uid; - const containerId = transaction.container?.id; - - const time = Math.round(transaction.timestamp.us / 1000); - const infraMetricsQuery = getInfraMetricsQuery(transaction); - - const infraConfigItems: InfraConfigItem[] = [ - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showPodLogsLinkLabel', - { defaultMessage: 'Show pod logs' } - ), - condition: !!podId, - app: 'logs', - path: `/link-to/pod-logs/${podId}`, - query: { time } - }, - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showContainerLogsLinkLabel', - { defaultMessage: 'Show container logs' } - ), - condition: !!containerId, - app: 'logs', - path: `/link-to/container-logs/${containerId}`, - query: { time } - }, - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showHostLogsLinkLabel', - { defaultMessage: 'Show host logs' } - ), - condition: !!hostName, - app: 'logs', - path: `/link-to/host-logs/${hostName}`, - query: { time } - }, - { - icon: 'logsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showTraceLogsLinkLabel', - { defaultMessage: 'Show trace logs' } - ), - condition: true, - app: 'logs', - path: `/link-to/logs`, - query: { - time, - filter: `trace.id:"${transaction.trace.id}" OR ${transaction.trace.id}` - } - }, - { - icon: 'metricsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showPodMetricsLinkLabel', - { defaultMessage: 'Show pod metrics' } - ), - condition: !!podId, - app: 'metrics', - path: `/link-to/pod-detail/${podId}`, - query: infraMetricsQuery - }, - { - icon: 'metricsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel', - { defaultMessage: 'Show container metrics' } - ), - condition: !!containerId, - app: 'metrics', - path: `/link-to/container-detail/${containerId}`, - query: infraMetricsQuery - }, - { - icon: 'metricsApp', - label: i18n.translate( - 'xpack.apm.transactionActionMenu.showHostMetricsLinkLabel', - { defaultMessage: 'Show host metrics' } - ), - condition: !!hostName, - app: 'metrics', - path: `/link-to/host-detail/${hostName}`, - query: infraMetricsQuery - } - ]; - - const infraItems = infraConfigItems.map( - ({ icon, label, condition, path, query, app }, index) => ({ - icon, - key: `infra-link-${index}`, - child: ( - - {label} - - ), - condition - }) - ); + const [isOpen, setIsOpen] = useState(false); - const uptimeLink = url.format({ - pathname: core.http.basePath.prepend('/app/uptime'), - hash: `/?${fromQuery( - pick( - { - dateRangeStart: urlParams.rangeFrom, - dateRangeEnd: urlParams.rangeTo, - search: `url.domain:"${transaction.url?.domain}"` - }, - (val: string) => !!val - ) - )}` + const sections = getSections({ + transaction, + basePath: core.http.basePath, + location, + urlParams }); - const menuItems = [ - ...infraItems, - { - icon: 'discoverApp', - key: 'discover-transaction', - condition: true, - child: ( - - {i18n.translate( - 'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel', - { - defaultMessage: 'View sample document' - } - )} - - ) - }, - { - icon: 'uptimeApp', - key: 'uptime', - child: ( - - {i18n.translate('xpack.apm.transactionActionMenu.viewInUptime', { - defaultMessage: 'View monitor status' - })} - - ), - condition: transaction.url?.domain - } - ] - .filter(({ condition }) => condition) - .map(({ icon, key, child }) => ( - - - {child} - - - - - - )); - return ( - setIsOpen(!isOpen)} />} - isOpen={isOpen} closePopover={() => setIsOpen(false)} + isOpen={isOpen} anchorPosition="downRight" - panelPaddingSize="none" + button={ setIsOpen(!isOpen)} />} > - - + {sections.map((section, idx) => { + const isLastSection = idx !== sections.length - 1; + return ( +
+ {section.map(item => ( +
+ {item.title && {item.title}} + {item.subtitle && ( + {item.subtitle} + )} + + {item.actions.map(action => ( + + ))} + +
+ ))} + {isLastSection && } +
+ ); + })} + ); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 2bfa5cf1274fa..e9f89034f58ee 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -36,7 +36,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show trace logs')).not.toBeNull(); + expect(queryByText('Trace logs')).not.toBeNull(); }); it('should not render the pod links when there is no pod id', async () => { @@ -44,8 +44,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show pod logs')).toBeNull(); - expect(queryByText('Show pod metrics')).toBeNull(); + expect(queryByText('Pod logs')).toBeNull(); + expect(queryByText('Pod metrics')).toBeNull(); }); it('should render the pod links when there is a pod id', async () => { @@ -53,8 +53,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithKubernetesData ); - expect(queryByText('Show pod logs')).not.toBeNull(); - expect(queryByText('Show pod metrics')).not.toBeNull(); + expect(queryByText('Pod logs')).not.toBeNull(); + expect(queryByText('Pod metrics')).not.toBeNull(); }); it('should not render the container links when there is no container id', async () => { @@ -62,8 +62,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show container logs')).toBeNull(); - expect(queryByText('Show container metrics')).toBeNull(); + expect(queryByText('Container logs')).toBeNull(); + expect(queryByText('Container metrics')).toBeNull(); }); it('should render the container links when there is a container id', async () => { @@ -71,8 +71,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithContainerData ); - expect(queryByText('Show container logs')).not.toBeNull(); - expect(queryByText('Show container metrics')).not.toBeNull(); + expect(queryByText('Container logs')).not.toBeNull(); + expect(queryByText('Container metrics')).not.toBeNull(); }); it('should not render the host links when there is no hostname', async () => { @@ -80,8 +80,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('Show host logs')).toBeNull(); - expect(queryByText('Show host metrics')).toBeNull(); + expect(queryByText('Host logs')).toBeNull(); + expect(queryByText('Host metrics')).toBeNull(); }); it('should render the host links when there is a hostname', async () => { @@ -89,8 +89,8 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithHostData ); - expect(queryByText('Show host logs')).not.toBeNull(); - expect(queryByText('Show host metrics')).not.toBeNull(); + expect(queryByText('Host logs')).not.toBeNull(); + expect(queryByText('Host metrics')).not.toBeNull(); }); it('should not render the uptime link if there is no url available', async () => { @@ -98,7 +98,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithMinimalData ); - expect(queryByText('View monitor status')).toBeNull(); + expect(queryByText('Status')).toBeNull(); }); it('should not render the uptime link if there is no domain available', async () => { @@ -106,7 +106,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithUrlWithoutDomain ); - expect(queryByText('View monitor status')).toBeNull(); + expect(queryByText('Status')).toBeNull(); }); it('should render the uptime link if there is a url with a domain', async () => { @@ -114,7 +114,7 @@ describe('TransactionActionMenu component', () => { Transactions.transactionWithUrlAndDomain ); - expect(queryByText('View monitor status')).not.toBeNull(); + expect(queryByText('Status')).not.toBeNull(); }); it('should match the snapshot', async () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts new file mode 100644 index 0000000000000..52c2d27eabb82 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -0,0 +1,204 @@ +/* + * 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 { Location } from 'history'; +import { getSections } from '../sections'; +import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; +import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; + +describe('Transaction action menu', () => { + const basePath = ({ + prepend: jest.fn() + } as unknown) as AppMountContextBasePath; + const date = '2020-02-06T11:00:00.000Z'; + const timestamp = { us: new Date(date).getTime() }; + + it('shows required sections only', () => { + const transaction = ({ + timestamp, + trace: { id: '123' }, + transaction: { id: '123' }, + '@timestamp': date + } as unknown) as Transaction; + expect( + getSections({ + transaction, + basePath, + location: ({} as unknown) as Location, + urlParams: {} + }) + ).toEqual([ + [ + { + key: 'traceDetails', + title: 'Trace details', + subtitle: 'View trace logs to get further details.', + actions: [ + { + key: 'traceLogs', + label: 'Trace logs', + href: + '#/link-to/logs?time=1580986800&filter=trace.id:%22123%22%20OR%20123', + condition: true + } + ] + } + ], + [ + { + key: 'kibana', + actions: [ + { + key: 'sampleDocument', + label: 'View sample document', + href: + '#/discover?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + condition: true + } + ] + } + ] + ]); + }); + + it('shows pod and required sections only', () => { + const transaction = ({ + kubernetes: { pod: { uid: '123' } }, + timestamp, + trace: { id: '123' }, + transaction: { id: '123' }, + '@timestamp': date + } as unknown) as Transaction; + expect( + getSections({ + transaction, + basePath, + location: ({} as unknown) as Location, + urlParams: {} + }) + ).toEqual([ + [ + { + key: 'podDetails', + title: 'Pod details', + subtitle: + 'View logs and metrics for this pod to get further details.', + actions: [ + { + key: 'podLogs', + label: 'Pod logs', + href: '#/link-to/pod-logs/123?time=1580986800', + condition: true + }, + { + key: 'podMetrics', + label: 'Pod metrics', + href: + '#/link-to/pod-detail/123?from=1580986500000&to=1580987100000', + condition: true + } + ] + }, + { + key: 'traceDetails', + title: 'Trace details', + subtitle: 'View trace logs to get further details.', + actions: [ + { + key: 'traceLogs', + label: 'Trace logs', + href: + '#/link-to/logs?time=1580986800&filter=trace.id:%22123%22%20OR%20123', + condition: true + } + ] + } + ], + [ + { + key: 'kibana', + actions: [ + { + key: 'sampleDocument', + label: 'View sample document', + href: + '#/discover?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + condition: true + } + ] + } + ] + ]); + }); + + it('shows host and required sections only', () => { + const transaction = ({ + host: { hostname: 'foo' }, + timestamp, + trace: { id: '123' }, + transaction: { id: '123' }, + '@timestamp': date + } as unknown) as Transaction; + expect( + getSections({ + transaction, + basePath, + location: ({} as unknown) as Location, + urlParams: {} + }) + ).toEqual([ + [ + { + key: 'hostDetails', + title: 'Host details', + subtitle: 'View host logs and metrics to get further details.', + actions: [ + { + key: 'hostLogs', + label: 'Host logs', + href: '#/link-to/host-logs/foo?time=1580986800', + condition: true + }, + { + key: 'hostMetrics', + label: 'Host metrics', + href: + '#/link-to/host-detail/foo?from=1580986500000&to=1580987100000', + condition: true + } + ] + }, + { + key: 'traceDetails', + title: 'Trace details', + subtitle: 'View trace logs to get further details.', + actions: [ + { + key: 'traceLogs', + label: 'Trace logs', + href: + '#/link-to/logs?time=1580986800&filter=trace.id:%22123%22%20OR%20123', + condition: true + } + ] + } + ], + [ + { + key: 'kibana', + actions: [ + { + key: 'sampleDocument', + label: 'View sample document', + href: + '#/discover?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + condition: true + } + ] + } + ] + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts new file mode 100644 index 0000000000000..31efdb6355563 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -0,0 +1,306 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Location } from 'history'; +import { pick, isEmpty } from 'lodash'; +import moment from 'moment'; +import url from 'url'; +import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; +import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; +import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; +import { getInfraHref } from '../Links/InfraLink'; +import { fromQuery } from '../Links/url_helpers'; +import { AppMountContextBasePath } from '../../../context/ApmPluginContext'; + +function getInfraMetricsQuery(transaction: Transaction) { + const timestamp = new Date(transaction['@timestamp']).getTime(); + const fiveMinutes = moment.duration(5, 'minutes').asMilliseconds(); + + return { + from: timestamp - fiveMinutes, + to: timestamp + fiveMinutes + }; +} + +interface Action { + key: string; + label: string; + href: string; + condition: boolean; +} + +interface Section { + key: string; + title?: string; + subtitle?: string; + actions: Action[]; +} + +type SectionRecord = Record; + +export const getSections = ({ + transaction, + basePath, + location, + urlParams +}: { + transaction: Transaction; + basePath: AppMountContextBasePath; + location: Location; + urlParams: IUrlParams; +}) => { + const hostName = transaction.host?.hostname; + const podId = transaction.kubernetes?.pod.uid; + const containerId = transaction.container?.id; + + const time = Math.round(transaction.timestamp.us / 1000); + const infraMetricsQuery = getInfraMetricsQuery(transaction); + + const uptimeLink = url.format({ + pathname: basePath.prepend('/app/uptime'), + hash: `/?${fromQuery( + pick( + { + dateRangeStart: urlParams.rangeFrom, + dateRangeEnd: urlParams.rangeTo, + search: `url.domain:"${transaction.url?.domain}"` + }, + (val: string) => !isEmpty(val) + ) + )}` + }); + + const podActions: Action[] = [ + { + key: 'podLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showPodLogsLinkLabel', + { defaultMessage: 'Pod logs' } + ), + href: getInfraHref({ + app: 'logs', + basePath, + path: `/link-to/pod-logs/${podId}`, + query: { time } + }), + condition: !!podId + }, + { + key: 'podMetrics', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showPodMetricsLinkLabel', + { defaultMessage: 'Pod metrics' } + ), + href: getInfraHref({ + app: 'metrics', + basePath, + path: `/link-to/pod-detail/${podId}`, + query: infraMetricsQuery + }), + condition: !!podId + } + ]; + + const containerActions: Action[] = [ + { + key: 'containerLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showContainerLogsLinkLabel', + { defaultMessage: 'Container logs' } + ), + href: getInfraHref({ + app: 'logs', + basePath, + path: `/link-to/container-logs/${containerId}`, + query: { time } + }), + condition: !!containerId + }, + { + key: 'containerMetrics', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel', + { defaultMessage: 'Container metrics' } + ), + href: getInfraHref({ + app: 'metrics', + basePath, + path: `/link-to/container-detail/${containerId}`, + query: infraMetricsQuery + }), + condition: !!containerId + } + ]; + + const hostActions: Action[] = [ + { + key: 'hostLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showHostLogsLinkLabel', + { defaultMessage: 'Host logs' } + ), + href: getInfraHref({ + app: 'logs', + basePath, + path: `/link-to/host-logs/${hostName}`, + query: { time } + }), + condition: !!hostName + }, + { + key: 'hostMetrics', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showHostMetricsLinkLabel', + { defaultMessage: 'Host metrics' } + ), + href: getInfraHref({ + app: 'metrics', + basePath, + path: `/link-to/host-detail/${hostName}`, + query: infraMetricsQuery + }), + condition: !!hostName + } + ]; + + const logActions: Action[] = [ + { + key: 'traceLogs', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.showTraceLogsLinkLabel', + { defaultMessage: 'Trace logs' } + ), + href: getInfraHref({ + app: 'logs', + basePath, + path: `/link-to/logs`, + query: { + time, + filter: `trace.id:"${transaction.trace.id}" OR ${transaction.trace.id}` + } + }), + condition: true + } + ]; + + const uptimeActions: Action[] = [ + { + key: 'monitorStatus', + label: i18n.translate('xpack.apm.transactionActionMenu.viewInUptime', { + defaultMessage: 'Status' + }), + href: uptimeLink, + condition: !!transaction.url?.domain + } + ]; + + const kibanaActions: Action[] = [ + { + key: 'sampleDocument', + label: i18n.translate( + 'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel', + { + defaultMessage: 'View sample document' + } + ), + href: getDiscoverHref({ + basePath, + query: getDiscoverQuery(transaction), + location + }), + condition: true + } + ]; + + const sectionRecord: SectionRecord = { + observability: [ + { + key: 'podDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.pod.title', { + defaultMessage: 'Pod details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.pod.subtitle', + { + defaultMessage: + 'View logs and metrics for this pod to get further details.' + } + ), + actions: podActions + }, + { + key: 'containerDetails', + title: i18n.translate( + 'xpack.apm.transactionActionMenu.container.title', + { + defaultMessage: 'Container details' + } + ), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.container.subtitle', + { + defaultMessage: + 'View logs and metrics for this container to get further details.' + } + ), + actions: containerActions + }, + { + key: 'hostDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.host.title', { + defaultMessage: 'Host details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.host.subtitle', + { + defaultMessage: 'View host logs and metrics to get further details.' + } + ), + actions: hostActions + }, + { + key: 'traceDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.trace.title', { + defaultMessage: 'Trace details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.trace.subtitle', + { + defaultMessage: 'View trace logs to get further details.' + } + ), + actions: logActions + }, + { + key: 'statusDetails', + title: i18n.translate('xpack.apm.transactionActionMenu.status.title', { + defaultMessage: 'Status details' + }), + subtitle: i18n.translate( + 'xpack.apm.transactionActionMenu.status.subtitle', + { + defaultMessage: 'View status to get further details.' + } + ), + actions: uptimeActions + } + ], + kibana: [{ key: 'kibana', actions: kibanaActions }] + }; + + // Filter out actions that shouldnt be shown and sections without any actions. + return Object.values(sectionRecord) + .map(sections => + sections + .map(section => ({ + ...section, + actions: section.actions.filter(action => action.condition) + })) + .filter(section => !isEmpty(section.actions)) + ) + .filter(sections => !isEmpty(sections)); +}; diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx index 86efd9b31974e..7a9aaa6dfb920 100644 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx +++ b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx @@ -8,6 +8,8 @@ import { createContext } from 'react'; import { AppMountContext, PackageInfo } from 'kibana/public'; import { ApmPluginSetupDeps, ConfigSchema } from '../new-platform/plugin'; +export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; + export interface ApmPluginContextValue { config: ConfigSchema; core: AppMountContext['core']; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index a07bd475cdfcb..55363ebe4d8f3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Subject } from 'rxjs'; import { Embeddable } from './embeddable'; import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; -import { Query, TimeRange, Filter } from 'src/plugins/data/public'; +import { Query, TimeRange, Filter, TimefilterContract } from 'src/plugins/data/public'; import { Document } from '../../persistence'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; jest.mock('../../../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, @@ -44,6 +46,7 @@ describe('embeddable', () => { it('should render expression with expression renderer', () => { const embeddable = new Embeddable( + dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, { editUrl: '', @@ -64,6 +67,7 @@ describe('embeddable', () => { const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; const embeddable = new Embeddable( + dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, { editUrl: '', @@ -89,6 +93,7 @@ describe('embeddable', () => { const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; const embeddable = new Embeddable( + dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, { editUrl: '', @@ -112,6 +117,7 @@ describe('embeddable', () => { const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; const embeddable = new Embeddable( + dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, { editUrl: '', @@ -130,4 +136,31 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(1); }); + + it('should re-render on auto refresh fetch observable', () => { + const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const query: Query = { language: 'kquery', query: '' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; + + const autoRefreshFetchSubject = new Subject(); + const timefilter = ({ + getAutoRefreshFetch$: () => autoRefreshFetchSubject.asObservable(), + } as unknown) as TimefilterContract; + + const embeddable = new Embeddable( + timefilter, + expressionRenderer, + { + editUrl: '', + editable: true, + savedVis, + }, + { id: '123', timeRange, query, filters } + ); + embeddable.render(mountpoint); + + autoRefreshFetchSubject.next(); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index a3a55f26ff7c2..252ba5c9bc0bc 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -7,7 +7,13 @@ import _ from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Query, TimeRange, Filter, IIndexPattern } from 'src/plugins/data/public'; +import { + Query, + TimeRange, + Filter, + IIndexPattern, + TimefilterContract, +} from 'src/plugins/data/public'; import { Subscription } from 'rxjs'; import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; import { @@ -43,6 +49,7 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(input)); this.onContainerStateChanged(initialInput); + + this.autoRefreshFetchSubscription = timefilter + .getAutoRefreshFetch$() + .subscribe(this.reload.bind(this)); } onContainerStateChanged(containerState: LensEmbeddableInput) { @@ -125,6 +137,7 @@ export class Embeddable extends AbstractEmbeddable, private savedObjectsClient: SavedObjectsClientContract, @@ -85,6 +90,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { ); return new Embeddable( + this.timefilter, this.expressionRenderer, { savedVis, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index cd121a1f96a2b..e606c69c8c386 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -14,6 +14,7 @@ import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/p import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types'; import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; export function createMockVisualization(): jest.Mocked { return { @@ -103,7 +104,7 @@ export function createExpressionRendererMock(): jest.Mock< export function createMockSetupDependencies() { return ({ - data: {}, + data: dataPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), } as unknown) as MockedSetupDependencies; @@ -111,11 +112,7 @@ export function createMockSetupDependencies() { export function createMockStartDependencies() { return ({ - data: { - indexPatterns: { - indexPatterns: {}, - }, - }, + data: dataPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createStartContract(), expressions: expressionsPluginMock.createStartContract(), } as unknown) as MockedStartDependencies; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx index 9a3d724705a1a..7a0bb3a2cc50f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx @@ -79,6 +79,7 @@ export class EditorFrameService { plugins.embeddable.registerEmbeddableFactory( 'lens', new EmbeddableFactory( + plugins.data.query.timefilter.timefilter, core.http, core.application.capabilities, core.savedObjects.client, diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index c16dc37097b13..f161e37efa8d8 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -236,26 +236,26 @@ class LinksMenuUI extends Component { let i = 0; findFieldType(datafeedIndices[i]); - function findFieldType(index) { - getFieldTypeFromMapping(index, categorizationFieldName) - .then(resp => { - if (resp !== '') { - createAndOpenUrl(index, resp); - } else { - i++; - if (i < datafeedIndices.length) { - findFieldType(datafeedIndices[i]); - } else { - error(); - } - } + const error = () => { + console.log( + `viewExamples(): error finding type of field ${categorizationFieldName} in indices:`, + datafeedIndices + ); + const { toasts } = this.props.kibana.services.notifications; + toasts.addDanger( + i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', { + defaultMessage: + 'Unable to view examples of documents with mlcategory {categoryId} ' + + 'as no mapping could be found for the categorization field {categorizationFieldName}', + values: { + categoryId, + categorizationFieldName, + }, }) - .catch(() => { - error(); - }); - } + ); + }; - function createAndOpenUrl(index, categorizationFieldType) { + const createAndOpenUrl = (index, categorizationFieldType) => { // Find the ID of the index pattern with a title attribute which matches the // index configured in the datafeed. If a Kibana index pattern has not been created // for this index, then the user will see a warning message on the Discover tab advising @@ -340,25 +340,25 @@ class LinksMenuUI extends Component { }) ); }); - } + }; - function error() { - console.log( - `viewExamples(): error finding type of field ${categorizationFieldName} in indices:`, - datafeedIndices - ); - const { toasts } = this.props.kibana.services.notifications; - toasts.addDanger( - i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', { - defaultMessage: - 'Unable to view examples of documents with mlcategory {categoryId} ' + - 'as no mapping could be found for the categorization field {categorizationFieldName}', - values: { - categoryId, - categorizationFieldName, - }, + function findFieldType(index) { + getFieldTypeFromMapping(index, categorizationFieldName) + .then(resp => { + if (resp !== '') { + createAndOpenUrl(index, resp); + } else { + i++; + if (i < datafeedIndices.length) { + findFieldType(datafeedIndices[i]); + } else { + error(); + } + } }) - ); + .catch(() => { + error(); + }); } }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts index 7ebbd45fd372a..2add1b6ea161c 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts @@ -6,3 +6,4 @@ export { useMlKibana, StartServices, MlKibanaReactContextValue } from './kibana_context'; export { useUiSettings } from './use_ui_settings_context'; +export { useTimefilter } from './use_timefilter'; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_timefilter.test.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_timefilter.test.ts new file mode 100644 index 0000000000000..98ddd874c695c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_timefilter.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useTimefilter } from './use_timefilter'; + +jest.mock('./kibana_context', () => ({ + useMlKibana: () => { + return { + services: { + data: { + query: { + timefilter: { + timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), + enableTimeRangeSelector: jest.fn(), + enableAutoRefreshSelector: jest.fn(), + }, + }, + }, + }, + }, + }; + }, +})); + +describe('useTimefilter', () => { + test('will not trigger any date picker settings by default', () => { + const { result } = renderHook(() => useTimefilter()); + const timefilter = result.current; + + expect(timefilter.disableTimeRangeSelector).toHaveBeenCalledTimes(0); + expect(timefilter.disableAutoRefreshSelector).toHaveBeenCalledTimes(0); + expect(timefilter.enableTimeRangeSelector).toHaveBeenCalledTimes(0); + expect(timefilter.enableTimeRangeSelector).toHaveBeenCalledTimes(0); + }); + + test('custom disabled overrides', () => { + const { result } = renderHook(() => + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }) + ); + const timefilter = result.current; + + expect(timefilter.disableTimeRangeSelector).toHaveBeenCalledTimes(1); + expect(timefilter.disableAutoRefreshSelector).toHaveBeenCalledTimes(1); + expect(timefilter.enableTimeRangeSelector).toHaveBeenCalledTimes(0); + expect(timefilter.enableTimeRangeSelector).toHaveBeenCalledTimes(0); + }); + + test('custom enabled overrides', () => { + const { result } = renderHook(() => + useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }) + ); + const timefilter = result.current; + + expect(timefilter.disableTimeRangeSelector).toHaveBeenCalledTimes(0); + expect(timefilter.disableAutoRefreshSelector).toHaveBeenCalledTimes(0); + expect(timefilter.enableTimeRangeSelector).toHaveBeenCalledTimes(1); + expect(timefilter.enableTimeRangeSelector).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_timefilter.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_timefilter.ts new file mode 100644 index 0000000000000..374e101f63dc8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_timefilter.ts @@ -0,0 +1,38 @@ +/* + * 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 { useEffect } from 'react'; + +import { useMlKibana } from './kibana_context'; + +interface UseTimefilterOptions { + timeRangeSelector?: boolean; + autoRefreshSelector?: boolean; +} + +export const useTimefilter = ({ + timeRangeSelector, + autoRefreshSelector, +}: UseTimefilterOptions = {}) => { + const { services } = useMlKibana(); + const { timefilter } = services.data.query.timefilter; + + useEffect(() => { + if (timeRangeSelector === true) { + timefilter.enableTimeRangeSelector(); + } else if (timeRangeSelector === false) { + timefilter.disableTimeRangeSelector(); + } + + if (autoRefreshSelector === true) { + timefilter.enableAutoRefreshSelector(); + } else if (autoRefreshSelector === false) { + timefilter.disableAutoRefreshSelector(); + } + }, [timeRangeSelector, autoRefreshSelector]); + + return timefilter; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index ae0c034f972d6..0f56f78c708ee 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { isFullLicense } from '../license/check_license'; -import { useMlKibana } from '../contexts/kibana'; +import { useTimefilter } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; @@ -49,10 +49,7 @@ function startTrialDescription() { } export const DatavisualizerSelector: FC = () => { - const { services } = useMlKibana(); - const { timefilter } = services.data.query.timefilter; - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const startTrialVisible = isFullLicense() === false; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 9dcb9d25692e9..5c32d62c39f84 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -7,7 +7,7 @@ import React, { FC, Fragment } from 'react'; import { IUiSettingsClient } from 'src/core/public'; -import { useMlKibana } from '../../contexts/kibana'; +import { useTimefilter } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; import { getIndexPatternsContract } from '../../util/index_utils'; @@ -19,10 +19,7 @@ export interface FileDataVisualizerPageProps { } export const FileDataVisualizerPage: FC = ({ kibanaConfig }) => { - const { services } = useMlKibana(); - const { timefilter } = services.data.query.timefilter; - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const indexPatterns = getIndexPatternsContract(); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index a6508ea868724..84c07651d323d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -38,7 +38,7 @@ import { FullTimeRangeSelector } from '../../components/full_time_range_selector import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useMlContext, SavedSearchQuery } from '../../contexts/ml'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; -import { useMlKibana } from '../../contexts/kibana'; +import { useTimefilter } from '../../contexts/kibana'; import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; import { TimeBuckets } from '../../util/time_buckets'; import { useUrlState } from '../../util/url_state'; @@ -97,11 +97,13 @@ function getDefaultPageState(): DataVisualizerPageState { } export const Page: FC = () => { - const { services } = useMlKibana(); const mlContext = useMlContext(); - const { timefilter } = services.data.query.timefilter; const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = mlContext; + const timefilter = useTimefilter({ + timeRangeSelector: currentIndexPattern.timeFieldName !== undefined, + autoRefreshSelector: true, + }); const dataLoader = new DataLoader(currentIndexPattern, kibanaConfig); const [globalState, setGlobalState] = useUrlState('_g'); @@ -122,13 +124,6 @@ export const Page: FC = () => { const [lastRefresh, setLastRefresh] = useState(0); useEffect(() => { - if (currentIndexPattern.timeFieldName !== undefined) { - timefilter.enableTimeRangeSelector(); - } else { - timefilter.disableTimeRangeSelector(); - } - - timefilter.enableAutoRefreshSelector(); timeBasedIndexCheck(currentIndexPattern, true); }, []); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx index b0046f7b8d699..2c6726338d2f1 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx @@ -29,7 +29,7 @@ import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; -import { useMlKibana } from '../../contexts/kibana'; +import { useTimefilter } from '../../contexts/kibana'; const breadcrumbs = [ ML_BREADCRUMB, @@ -70,8 +70,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [appState, setAppState] = useUrlState('_a'); const [globalState, setGlobalState] = useUrlState('_g'); const [lastRefresh, setLastRefresh] = useState(0); - const { services } = useMlKibana(); - const { timefilter } = services.data.query.timefilter; + const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }); const { jobIds } = useJobSelection(jobsWithTimeRange, getDateFormatTz()); @@ -111,9 +110,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim }, [globalState?.time?.from, globalState?.time?.to]); useEffect(() => { - timefilter.enableTimeRangeSelector(); - timefilter.enableAutoRefreshSelector(); - const viewByFieldName = appState?.mlExplorerSwimlane?.viewByFieldName; if (viewByFieldName !== undefined) { explorerService.setViewBySwimlaneFieldName(viewByFieldName); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx index ca2c0750397e5..c1d686d356dda 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -14,8 +14,8 @@ import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { JobsPage } from '../../jobs/jobs_list'; +import { useTimefilter } from '../../contexts/kibana'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; -import { useMlKibana } from '../../contexts/kibana'; const breadcrumbs = [ ML_BREADCRUMB, @@ -36,8 +36,7 @@ export const jobListRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); - const { services } = useMlKibana(); - const { timefilter } = services.data.query.timefilter; + const timefilter = useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true }); const [globalState, setGlobalState] = useUrlState('_g'); @@ -48,9 +47,6 @@ const PageWrapper: FC = ({ deps }) => { const blockRefresh = refreshValue === 0 || refreshPause === true; useEffect(() => { - timefilter.disableTimeRangeSelector(); - timefilter.enableAutoRefreshSelector(); - // If the refreshInterval defaults to 0s/pause=true, set it to 30s/pause=false, // otherwise pass on the globalState's settings to the date picker. const refreshInterval = diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx index 85227c11582d9..b1e00158efb94 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx @@ -16,6 +16,7 @@ import { checkFullLicense } from '../../license/check_license'; import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; +import { useTimefilter } from '../../contexts/kibana'; import { ML_BREADCRUMB } from '../breadcrumbs'; const breadcrumbs = [ @@ -41,6 +42,7 @@ const PageWrapper: FC = ({ deps }) => { getMlNodeCount, loadMlServerInfo, }); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index fdbfcb3397c75..c1bfaa2fe6c1e 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; +import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license/check_license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; @@ -45,6 +46,8 @@ const PageWrapper: FC = ({ deps }) => { getMlNodeCount, }); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const canCreateCalendar = checkPermission('canCreateCalendar'); const canDeleteCalendar = checkPermission('canDeleteCalendar'); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index 7f622a1bba62b..7af2e49e3a69e 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; +import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license/check_license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; @@ -77,6 +78,8 @@ const PageWrapper: FC = ({ location, mode, deps }) => { checkMlNodesAvailable, }); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const canCreateCalendar = checkPermission('canCreateCalendar'); const canDeleteCalendar = checkPermission('canDeleteCalendar'); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 6a4ce271bff17..9c5c06b76247c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; +import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license/check_license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; @@ -46,6 +47,8 @@ const PageWrapper: FC = ({ deps }) => { getMlNodeCount, }); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const canCreateFilter = checkPermission('canCreateFilter'); const canDeleteFilter = checkPermission('canDeleteFilter'); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 4fa15ebaac21a..752b889490e58 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; +import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license/check_license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; @@ -77,6 +78,8 @@ const PageWrapper: FC = ({ location, mode, deps }) => { checkMlNodesAvailable, }); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const canCreateFilter = checkPermission('canCreateFilter'); const canDeleteFilter = checkPermission('canDeleteFilter'); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx index 846512503ede5..10efb2dcc60c7 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -14,6 +14,7 @@ import React, { FC } from 'react'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; +import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license/check_license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; @@ -35,6 +36,8 @@ const PageWrapper: FC = ({ deps }) => { getMlNodeCount, }); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const canGetFilters = checkPermission('canGetFilters'); const canGetCalendars = checkPermission('canGetCalendars'); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index 0ae42aa44e089..8633947374a8b 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -12,33 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer'; -jest.mock('ui/new_platform', () => ({ - npStart: { - plugins: { - data: { - query: { - timefilter: { - timefilter: { - enableTimeRangeSelector: jest.fn(), - enableAutoRefreshSelector: jest.fn(), - getRefreshInterval: jest.fn(), - setRefreshInterval: jest.fn(), - getTime: jest.fn(), - isAutoRefreshSelectorEnabled: jest.fn(), - isTimeRangeSelectorEnabled: jest.fn(), - getRefreshIntervalUpdate$: jest.fn(), - getTimeUpdate$: jest.fn(), - getEnabledUpdated$: jest.fn(), - }, - history: { get: jest.fn() }, - }, - }, - }, - }, - }, -})); - -jest.mock('../../contexts/kibana', () => ({ +jest.mock('../../contexts/kibana/kibana_context', () => ({ useMlKibana: () => { return { services: { @@ -47,6 +21,8 @@ jest.mock('../../contexts/kibana', () => ({ query: { timefilter: { timefilter: { + disableTimeRangeSelector: jest.fn(), + disableAutoRefreshSelector: jest.fn(), enableTimeRangeSelector: jest.fn(), enableAutoRefreshSelector: jest.fn(), getRefreshInterval: jest.fn(), diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 5bc2435db078c..f8a6f6c454fc0 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -35,7 +35,7 @@ import { useRefresh } from '../use_refresh'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; -import { useMlKibana } from '../../contexts/kibana'; +import { useTimefilter } from '../../contexts/kibana'; export const timeSeriesExplorerRoute: MlRoute = { path: '/timeseriesexplorer', @@ -88,8 +88,7 @@ export const TimeSeriesExplorerUrlStateManager: FC(); - const { services } = useMlKibana(); - const { timefilter } = services.data.query.timefilter; + const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }); const refresh = useRefresh(); useEffect(() => { @@ -106,11 +105,6 @@ export const TimeSeriesExplorerUrlStateManager: FC { - timefilter.enableTimeRangeSelector(); - timefilter.enableAutoRefreshSelector(); - }, []); - // We cannot simply infer bounds from the globalState's `time` attribute // with `moment` since it can contain custom strings such as `now-15m`. // So when globalState's `time` changes, we update the timefilter and use diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 0489528fa0f63..935e67ec05eff 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -50,9 +50,6 @@ class NewCalendarUI extends Component { } componentDidMount() { - const { timefilter } = this.props.kibana.services.data.query.timefilter; - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); this.formSetup(); } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 6f409e70e68b8..70b855e80a770 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -8,11 +8,7 @@ import { datafeedsProvider } from './datafeeds'; import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; import { newJobCapsProvider } from './new_job_caps'; -import { - newJobChartsProvider, - categorizationExamplesProvider, - topCategoriesProvider, -} from './new_job'; +import { newJobChartsProvider, topCategoriesProvider } from './new_job'; export function jobServiceProvider(callAsCurrentUser) { return { @@ -21,7 +17,6 @@ export function jobServiceProvider(callAsCurrentUser) { ...groupsProvider(callAsCurrentUser), ...newJobCapsProvider(callAsCurrentUser), ...newJobChartsProvider(callAsCurrentUser), - ...categorizationExamplesProvider(callAsCurrentUser), ...topCategoriesProvider(callAsCurrentUser), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index 76473bd55db7f..ea2c71b04f56d 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -17,7 +17,10 @@ import { ValidationResults } from './validation_results'; const CHUNK_SIZE = 100; -export function categorizationExamplesProvider(callWithRequest: callWithRequestType) { +export function categorizationExamplesProvider( + callWithRequest: callWithRequestType, + callWithInternalUser: callWithRequestType +) { const validationResults = new ValidationResults(); async function categorizationExamples( @@ -109,7 +112,7 @@ export function categorizationExamplesProvider(callWithRequest: callWithRequestT } async function loadTokens(examples: string[], analyzer: CategorizationAnalyzer) { - const { tokens }: { tokens: Token[] } = await callWithRequest('indices.analyze', { + const { tokens }: { tokens: Token[] } = await callWithInternalUser('indices.analyze', { body: { ...getAnalyzer(analyzer), text: examples, diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts index b37fcba737802..deb62678a777c 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts @@ -6,18 +6,6 @@ import { schema } from '@kbn/config-schema'; -const analyzerSchema = { - tokenizer: schema.string(), - filter: schema.maybe( - schema.arrayOf( - schema.object({ - type: schema.string(), - stopwords: schema.arrayOf(schema.maybe(schema.string())), - }) - ) - ), -}; - export const categorizationFieldExamplesSchema = { indexPatternTitle: schema.string(), query: schema.any(), @@ -26,7 +14,7 @@ export const categorizationFieldExamplesSchema = { timeField: schema.maybe(schema.string()), start: schema.number(), end: schema.number(), - analyzer: schema.object(analyzerSchema), + analyzer: schema.any(), }; export const chartSchema = { diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index be1554bf55f78..35215f8008ec3 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -76,6 +76,8 @@ "CreateFilter", "UpdateFilter", "DeleteFilter", - "GetFiltersStats" + "GetFiltersStats", + "Indices", + "FieldCaps" ] } diff --git a/x-pack/legacy/plugins/ml/server/routes/indices.js b/x-pack/legacy/plugins/ml/server/routes/indices.js deleted file mode 100644 index 309b41e53eef5..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/indices.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; - -export function indicesRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'POST', - path: '/api/ml/indices/field_caps', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const index = request.payload.index; - let fields = '*'; - if (request.payload.fields !== undefined && Array.isArray(request.payload.fields)) { - fields = request.payload.fields.join(','); - } - - return callWithRequest('fieldCaps', { index, fields }).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/indices.ts b/x-pack/legacy/plugins/ml/server/routes/indices.ts new file mode 100644 index 0000000000000..0ee15f1321e9c --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/indices.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapError } from '../client/error_wrapper'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { RouteInitialization } from '../new_platform/plugin'; + +/** + * Indices routes. + */ +export function indicesRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup Indices + * + * @api {post} /api/ml/indices/field_caps + * @apiName FieldCaps + * @apiDescription Retrieves the capabilities of fields among multiple indices. + */ + router.post( + { + path: '/api/ml/indices/field_caps', + validate: { + body: schema.object({ + index: schema.maybe(schema.string()), + fields: schema.maybe(schema.arrayOf(schema.string())), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + body: { index, fields: requestFields }, + } = request; + const fields = + requestFields !== undefined && Array.isArray(requestFields) + ? requestFields.join(',') + : '*'; + const result = await context.ml!.mlClient.callAsCurrentUser('fieldCaps', { index, fields }); + return response.ok({ body: result }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.ts b/x-pack/legacy/plugins/ml/server/routes/job_service.ts index 9aa3960e59e4c..5ddbd4cdfd5a5 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.ts +++ b/x-pack/legacy/plugins/ml/server/routes/job_service.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; import { schema } from '@kbn/config-schema'; +import { IScopedClusterClient } from 'src/core/server'; import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../new_platform/plugin'; +import { isSecurityDisabled } from '../lib/security_utils'; import { categorizationFieldExamplesSchema, chartSchema, @@ -21,11 +24,31 @@ import { } from '../new_platform/job_service_schema'; // @ts-ignore no declaration module import { jobServiceProvider } from '../models/job_service'; +import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { + async function hasPermissionToCreateJobs( + callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] + ) { + if (isSecurityDisabled(xpackMainPlugin) === true) { + return true; + } + + const resp = await callAsCurrentUser('ml.privilegeCheck', { + body: { + cluster: [ + 'cluster:admin/xpack/ml/job/put', + 'cluster:admin/xpack/ml/job/open', + 'cluster:admin/xpack/ml/datafeeds/put', + ], + }, + }); + return resp.has_all_requested; + } + /** * @apiGroup JobService * @@ -545,8 +568,17 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio }, licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { try { - const { validateCategoryExamples } = jobServiceProvider( - context.ml!.mlClient.callAsCurrentUser + // due to the use of the _analyze endpoint which is called by the kibana user, + // basic job creation privileges are required to use this endpoint + if ((await hasPermissionToCreateJobs(context.ml!.mlClient.callAsCurrentUser)) === false) { + throw Boom.forbidden( + 'Insufficient privileges, the machine_learning_admin role is required.' + ); + } + + const { validateCategoryExamples } = categorizationExamplesProvider( + context.ml!.mlClient.callAsCurrentUser, + context.ml!.mlClient.callAsInternalUser ); const { indexPatternTitle, diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/fixtures/beats_stats_results.json b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/beats_stats_results.json similarity index 100% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/fixtures/beats_stats_results.json rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/beats_stats_results.json diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/create_query.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.test.ts similarity index 89% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/create_query.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.test.ts index 63c779ab4b520..a85d084f83d83 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/create_query.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.test.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { set } from 'lodash'; -import { createTypeFilter, createQuery } from '../create_query.js'; +import { createTypeFilter, createQuery } from './create_query'; describe('Create Type Filter', () => { it('Builds a type filter syntax', () => { const typeFilter = createTypeFilter('my_type'); - expect(typeFilter).to.eql({ + expect(typeFilter).toStrictEqual({ bool: { should: [{ term: { _type: 'my_type' } }, { term: { type: 'my_type' } }] }, }); }); @@ -36,7 +35,7 @@ describe('Create Query', () => { ], }, }; - expect(result).to.be.eql(expected); + expect(result).toStrictEqual(expected); }); it('Uses `type` option to add type filter with minimal fields', () => { @@ -47,7 +46,7 @@ describe('Create Query', () => { { term: { _type: 'test-type-yay' } }, { term: { type: 'test-type-yay' } }, ]); - expect(result).to.be.eql(expected); + expect(result).toStrictEqual(expected); }); it('Uses `type` option to add type filter with all other option fields', () => { @@ -77,6 +76,6 @@ describe('Create Query', () => { ], }, }; - expect(result).to.be.eql(expected); + expect(result).toStrictEqual(expected); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.ts similarity index 80% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.ts index 6fcbb677b307d..9a801094458bd 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaults } from 'lodash'; import moment from 'moment'; /* @@ -14,7 +13,7 @@ import moment from 'moment'; * TODO: this backwards compatibility helper will only be supported for 5.x-6. This * function should be removed in 7.0 */ -export const createTypeFilter = type => { +export const createTypeFilter = (type: string) => { return { bool: { should: [{ term: { _type: type } }, { term: { type } }], @@ -22,6 +21,18 @@ export const createTypeFilter = type => { }; }; +export interface QueryOptions { + type?: string; + filters?: object[]; + clusterUuid?: string; + start?: string | number; + end?: string | number; +} + +interface RangeFilter { + range: { [key: string]: { format?: string; gte?: string | number; lte?: string | number } }; +} + /* * Creates the boilerplace for querying monitoring data, including filling in * start time and end time, and injecting additional filters. @@ -36,9 +47,8 @@ export const createTypeFilter = type => { * @param {Date} options.start - numeric timestamp (optional) * @param {Date} options.end - numeric timestamp (optional) */ -export function createQuery(options) { - options = defaults(options, { filters: [] }); - const { type, clusterUuid, start, end, filters } = options; +export function createQuery(options: QueryOptions) { + const { type, clusterUuid, start, end, filters = [] } = options; let typeFilter; if (type) { @@ -50,7 +60,7 @@ export function createQuery(options) { clusterUuidFilter = { term: { cluster_uuid: clusterUuid } }; } - let timeRangeFilter; + let timeRangeFilter: RangeFilter | undefined; if (start || end) { timeRangeFilter = { range: { diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts similarity index 78% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index f27fde50242f4..470642f9dd8a3 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import sinon from 'sinon'; -import { addStackStats, getAllStats, handleAllStats } from '../get_all_stats'; +import { addStackStats, getAllStats, handleAllStats } from './get_all_stats'; +import { ESClusterStats } from './get_es_stats'; +import { KibanaStats } from './get_kibana_stats'; +import { ClustersHighLevelStats } from './get_high_level_stats'; -// FAILING: https://github.com/elastic/kibana/issues/51371 -describe.skip('get_all_stats', () => { +describe('get_all_stats', () => { const size = 123; const start = 0; const end = 1; @@ -100,9 +101,6 @@ describe.skip('get_all_stats', () => { describe('getAllStats', () => { it('returns clusters', async () => { - const clusterUuidsResponse = { - aggregations: { cluster_uuids: { buckets: [{ key: 'a' }] } }, - }; const esStatsResponse = { hits: { hits: [{ _id: 'a', _source: { cluster_uuid: 'a' } }], @@ -177,15 +175,25 @@ describe.skip('get_all_stats', () => { callCluster .withArgs('search') .onCall(0) - .returns(Promise.resolve(clusterUuidsResponse)) - .onCall(1) .returns(Promise.resolve(esStatsResponse)) - .onCall(2) + .onCall(1) .returns(Promise.resolve(kibanaStatsResponse)) + .onCall(2) + .returns(Promise.resolve(logstashStatsResponse)) .onCall(3) - .returns(Promise.resolve(logstashStatsResponse)); + .returns(Promise.resolve({})) // Beats stats + .onCall(4) + .returns(Promise.resolve({})); // Beats state - expect(await getAllStats({ callCluster, server, start, end })).to.eql(allClusters); + expect( + await getAllStats([{ clusterUuid: 'a' }], { + callCluster: callCluster as any, + usageCollection: {} as any, + server, + start, + end, + }) + ).toStrictEqual(allClusters); }); it('returns empty clusters', async () => { @@ -195,21 +203,33 @@ describe.skip('get_all_stats', () => { callCluster.withArgs('search').returns(Promise.resolve(clusterUuidsResponse)); - expect(await getAllStats({ callCluster, server, start, end })).to.eql([]); + expect( + await getAllStats([], { + callCluster: callCluster as any, + usageCollection: {} as any, + server, + start, + end, + }) + ).toStrictEqual([]); }); }); describe('handleAllStats', () => { it('handles response', () => { - const clusters = handleAllStats(esClusters, { kibana: kibanaStats, logstash: logstashStats }); + const clusters = handleAllStats(esClusters as ESClusterStats[], { + kibana: (kibanaStats as unknown) as KibanaStats, + logstash: (logstashStats as unknown) as ClustersHighLevelStats, + beats: {}, + }); - expect(clusters).to.eql(expectedClusters); + expect(clusters).toStrictEqual(expectedClusters); }); it('handles no clusters response', () => { - const clusters = handleAllStats([], {}); + const clusters = handleAllStats([], {} as any); - expect(clusters).to.have.length(0); + expect(clusters).toHaveLength(0); }); }); @@ -230,9 +250,9 @@ describe.skip('get_all_stats', () => { }, }; - addStackStats(cluster, stats, 'xyz'); + addStackStats(cluster as ESClusterStats, stats, 'xyz'); - expect(cluster.stack_stats.xyz).to.be(stats.a); + expect((cluster as any).stack_stats.xyz).toStrictEqual(stats.a); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts similarity index 66% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index 87281a19141ae..aa5e937387daf 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -6,22 +6,26 @@ import { get, set, merge } from 'lodash'; +import { StatsGetter } from 'src/legacy/core_plugins/telemetry/server/collection_manager'; import { LOGSTASH_SYSTEM_ID, KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../common/constants'; -import { getElasticsearchStats } from './get_es_stats'; -import { getKibanaStats } from './get_kibana_stats'; +import { getElasticsearchStats, ESClusterStats } from './get_es_stats'; +import { getKibanaStats, KibanaStats } from './get_kibana_stats'; import { getBeatsStats } from './get_beats_stats'; import { getHighLevelStats } from './get_high_level_stats'; +type PromiseReturnType any> = ReturnType extends Promise + ? R + : T; + /** * Get statistics for all products joined by Elasticsearch cluster. + * Returns the array of clusters joined with the Kibana and Logstash instances. * - * @param {Object} server The Kibana server instance used to call ES as the internal user - * @param {function} callCluster The callWithRequest or callWithInternalUser handler - * @param {Date} start The starting range to request data - * @param {Date} end The ending range to request data - * @return {Promise} The array of clusters joined with the Kibana and Logstash instances. */ -export async function getAllStats(clustersDetails, { server, callCluster, start, end }) { +export const getAllStats: StatsGetter = async ( + clustersDetails, + { server, callCluster, start, end } +) => { const clusterUuids = clustersDetails.map(clusterDetails => clusterDetails.clusterUuid); const [esClusters, kibana, logstash, beats] = await Promise.all([ @@ -32,7 +36,7 @@ export async function getAllStats(clustersDetails, { server, callCluster, start, ]); return handleAllStats(esClusters, { kibana, logstash, beats }); -} +}; /** * Combine the statistics from the stack to create "cluster" stats that associate all products together based on the cluster @@ -41,9 +45,21 @@ export async function getAllStats(clustersDetails, { server, callCluster, start, * @param {Array} clusters The Elasticsearch clusters * @param {Object} kibana The Kibana instances keyed by Cluster UUID * @param {Object} logstash The Logstash nodes keyed by Cluster UUID - * @return {Array} The clusters joined with the Kibana and Logstash instances under each cluster's {@code stack_stats}. + * + * Returns the clusters joined with the Kibana and Logstash instances under each cluster's {@code stack_stats}. */ -export function handleAllStats(clusters, { kibana, logstash, beats }) { +export function handleAllStats( + clusters: ESClusterStats[], + { + kibana, + logstash, + beats, + }: { + kibana: KibanaStats; + logstash: PromiseReturnType; + beats: PromiseReturnType; + } +) { return clusters.map(cluster => { // if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats addStackStats(cluster, kibana, KIBANA_SYSTEM_ID); @@ -62,8 +78,12 @@ export function handleAllStats(clusters, { kibana, logstash, beats }) { * @param {Object} allProductStats Product stats, keyed by Cluster UUID * @param {String} product The product name being added (e.g., 'kibana' or 'logstash') */ -export function addStackStats(cluster, allProductStats, product) { - const productStats = get(allProductStats, cluster.cluster_uuid); +export function addStackStats( + cluster: ESClusterStats & { stack_stats?: { [product: string]: K } }, + allProductStats: T, + product: string +) { + const productStats = allProductStats[cluster.cluster_uuid]; // Don't add it if they're not using (or configured to report stats) this product for this cluster if (productStats) { @@ -75,12 +95,20 @@ export function addStackStats(cluster, allProductStats, product) { } } -export function mergeXPackStats(cluster, allProductStats, path, product) { +export function mergeXPackStats( + cluster: ESClusterStats & { stack_stats?: { xpack?: { [product: string]: unknown } } }, + allProductStats: T, + path: string, + product: string +) { const productStats = get(allProductStats, cluster.cluster_uuid + '.' + path); if (productStats || productStats === 0) { - if (!get(cluster, 'stack_stats.xpack')) { - set(cluster, 'stack_stats.xpack', {}); + if (!cluster.stack_stats) { + cluster.stack_stats = {}; + } + if (!cluster.stack_stats.xpack) { + cluster.stack_stats.xpack = {}; } const mergeStats = {}; diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_beats_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.test.ts similarity index 75% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_beats_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.test.ts index 522be71555fba..30888e1af3f53 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_beats_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fetchBeatsStats, processResults } from '../get_beats_stats'; +import { fetchBeatsStats, processResults } from './get_beats_stats'; import sinon from 'sinon'; -import expect from '@kbn/expect'; -import beatsStatsResultSet from './fixtures/beats_stats_results'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const beatsStatsResultSet = require('./__mocks__/fixtures/beats_stats_results'); const getBaseOptions = () => ({ clusters: {}, @@ -22,8 +22,8 @@ describe('Get Beats Stats', () => { const clusterUuids = ['aCluster', 'bCluster', 'cCluster']; const start = 100; const end = 200; - let server; - let callCluster; + let server = { config: () => ({ get: sinon.stub() }) }; + let callCluster = sinon.stub(); beforeEach(() => { const getStub = { get: sinon.stub() }; @@ -32,34 +32,34 @@ describe('Get Beats Stats', () => { callCluster = sinon.stub(); }); - it('should set `from: 0, to: 10000` in the query', () => { - fetchBeatsStats(server, callCluster, clusterUuids, start, end); + it('should set `from: 0, to: 10000` in the query', async () => { + await fetchBeatsStats(server, callCluster, clusterUuids, start, end, {} as any); const { args } = callCluster.firstCall; const [api, { body }] = args; - expect(api).to.be('search'); - expect(body.from).to.be(0); - expect(body.size).to.be(10000); + expect(api).toEqual('search'); + expect(body.from).toEqual(0); + expect(body.size).toEqual(10000); }); - it('should set `from: 10000, from: 10000` in the query', () => { - fetchBeatsStats(server, callCluster, clusterUuids, start, end, { page: 1 }); + it('should set `from: 10000, from: 10000` in the query', async () => { + await fetchBeatsStats(server, callCluster, clusterUuids, start, end, { page: 1 } as any); const { args } = callCluster.firstCall; const [api, { body }] = args; - expect(api).to.be('search'); - expect(body.from).to.be(10000); - expect(body.size).to.be(10000); + expect(api).toEqual('search'); + expect(body.from).toEqual(10000); + expect(body.size).toEqual(10000); }); - it('should set `from: 20000, from: 10000` in the query', () => { - fetchBeatsStats(server, callCluster, clusterUuids, start, end, { page: 2 }); + it('should set `from: 20000, from: 10000` in the query', async () => { + await fetchBeatsStats(server, callCluster, clusterUuids, start, end, { page: 2 } as any); const { args } = callCluster.firstCall; const [api, { body }] = args; - expect(api).to.be('search'); - expect(body.from).to.be(20000); - expect(body.size).to.be(10000); + expect(api).toEqual('search'); + expect(body.from).toEqual(20000); + expect(body.size).toEqual(10000); }); }); @@ -68,9 +68,9 @@ describe('Get Beats Stats', () => { const resultsEmpty = undefined; const options = getBaseOptions(); - processResults(resultsEmpty, options); + processResults(resultsEmpty as any, options); - expect(options.clusters).to.eql({}); + expect(options.clusters).toStrictEqual({}); }); it('should summarize single result with some missing fields', () => { @@ -92,9 +92,9 @@ describe('Get Beats Stats', () => { }; const options = getBaseOptions(); - processResults(results, options); + processResults(results as any, options); - expect(options.clusters).to.eql({ + expect(options.clusters).toStrictEqual({ FlV4ckTxQ0a78hmBkzzc9A: { count: 1, versions: {}, @@ -122,11 +122,11 @@ describe('Get Beats Stats', () => { const options = getBaseOptions(); // beatsStatsResultSet is an array of many small query results - beatsStatsResultSet.forEach(results => { + beatsStatsResultSet.forEach((results: any) => { processResults(results, options); }); - expect(options.clusters).to.eql({ + expect(options.clusters).toStrictEqual({ W7hppdX7R229Oy3KQbZrTw: { count: 5, versions: { '7.0.0-alpha1': 5 }, diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts similarity index 60% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts index 5722228b60207..975a3bfee6333 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.ts @@ -5,6 +5,8 @@ */ import { get } from 'lodash'; +import { StatsCollectionConfig } from 'src/legacy/core_plugins/telemetry/server/collection_manager'; +import { SearchResponse } from 'elasticsearch'; import { createQuery } from './create_query'; import { INDEX_PATTERN_BEATS } from '../../common/constants'; @@ -33,6 +35,107 @@ const getBaseStats = () => ({ }, }); +export interface BeatsStats { + cluster_uuid: string; + beats_stats?: { + beat?: { + version?: string; + type?: string; + host?: string; + }; + metrics?: { + libbeat?: { + output?: { + type?: string; + }; + pipeline?: { + events?: { + published?: number; + }; + }; + }; + }; + }; + beats_state?: { + beat?: { + type?: string; + }; + state?: { + input?: { + names: string[]; + count: number; + }; + module?: { + names: string[]; + count: number; + }; + heartbeat?: HeartbeatBase; + functionbeat?: { + functions?: { + count?: number; + }; + }; + host?: { + architecture: string; + os: { platform: string }; + }; + }; + }; +} + +interface HeartbeatBase { + monitors: number; + endpoints: number; + // I have to add the '| number' bit because otherwise TS complains about 'monitors' and 'endpoints' not being of type HeartbeatBase + [key: string]: HeartbeatBase | number | undefined; +} + +export interface BeatsBaseStats { + // stats + versions: { [version: string]: number }; + types: { [type: string]: number }; + outputs: { [outputType: string]: number }; + count: number; + eventsPublished: number; + hosts: number; + // state + input: { + count: number; + names: string[]; + }; + module: { + count: number; + names: string[]; + }; + architecture: { + count: number; + architectures: BeatsArchitecture[]; + }; + heartbeat?: HeartbeatBase; + functionbeat?: { + functions: { + count: number; + }; + }; +} + +export interface BeatsProcessOptions { + clusters: { [clusterUuid: string]: BeatsBaseStats }; // the result object to be built up + clusterHostSets: { [clusterUuid: string]: Set }; // passed to processResults for tracking state in the results generation + clusterInputSets: { [clusterUuid: string]: Set }; // passed to processResults for tracking state in the results generation + clusterModuleSets: { [clusterUuid: string]: Set }; // passed to processResults for tracking state in the results generation + clusterArchitectureMaps: { + // passed to processResults for tracking state in the results generation + [clusterUuid: string]: Map; + }; +} + +export interface BeatsArchitecture { + name: string; + architecture: string; + count: number; +} + /* * Update a clusters object with processed beat stats * @param {Array} results - array of Beats docs from ES @@ -41,12 +144,18 @@ const getBaseStats = () => ({ * @param {Object} clusterModuleSets - the object keyed by cluster UUIDs to count the unique modules */ export function processResults( - results = [], - { clusters, clusterHostSets, clusterInputSets, clusterModuleSets, clusterArchitectureMaps } + results: SearchResponse, + { + clusters, + clusterHostSets, + clusterInputSets, + clusterModuleSets, + clusterArchitectureMaps, + }: BeatsProcessOptions ) { - const currHits = get(results, 'hits.hits', []); + const currHits = results?.hits?.hits || []; currHits.forEach(hit => { - const clusterUuid = get(hit, '_source.cluster_uuid'); + const clusterUuid = hit._source.cluster_uuid; if (clusters[clusterUuid] === undefined) { clusters[clusterUuid] = getBaseStats(); clusterHostSets[clusterUuid] = new Set(); @@ -57,30 +166,30 @@ export function processResults( const processBeatsStatsResults = () => { const { versions, types, outputs } = clusters[clusterUuid]; - const thisVersion = get(hit, '_source.beats_stats.beat.version'); + const thisVersion = hit._source.beats_stats?.beat?.version; if (thisVersion !== undefined) { const thisVersionAccum = versions[thisVersion] || 0; versions[thisVersion] = thisVersionAccum + 1; } - const thisType = get(hit, '_source.beats_stats.beat.type'); + const thisType = hit._source.beats_stats?.beat?.type; if (thisType !== undefined) { const thisTypeAccum = types[thisType] || 0; types[thisType] = thisTypeAccum + 1; } - const thisOutput = get(hit, '_source.beats_stats.metrics.libbeat.output.type'); + const thisOutput = hit._source.beats_stats?.metrics?.libbeat?.output?.type; if (thisOutput !== undefined) { const thisOutputAccum = outputs[thisOutput] || 0; outputs[thisOutput] = thisOutputAccum + 1; } - const thisEvents = get(hit, '_source.beats_stats.metrics.libbeat.pipeline.events.published'); + const thisEvents = hit._source.beats_stats?.metrics?.libbeat?.pipeline?.events?.published; if (thisEvents !== undefined) { clusters[clusterUuid].eventsPublished += thisEvents; } - const thisHost = get(hit, '_source.beats_stats.beat.host'); + const thisHost = hit._source.beats_stats?.beat?.host; if (thisHost !== undefined) { const hostsMap = clusterHostSets[clusterUuid]; hostsMap.add(thisHost); @@ -89,7 +198,7 @@ export function processResults( }; const processBeatsStateResults = () => { - const stateInput = get(hit, '_source.beats_state.state.input'); + const stateInput = hit._source.beats_state?.state?.input; if (stateInput !== undefined) { const inputSet = clusterInputSets[clusterUuid]; stateInput.names.forEach(name => inputSet.add(name)); @@ -97,8 +206,8 @@ export function processResults( clusters[clusterUuid].input.count += stateInput.count; } - const stateModule = get(hit, '_source.beats_state.state.module'); - const statsType = get(hit, '_source.beats_state.beat.type'); + const stateModule = hit._source.beats_state?.state?.module; + const statsType = hit._source.beats_state?.beat?.type; if (stateModule !== undefined) { const moduleSet = clusterModuleSets[clusterUuid]; stateModule.names.forEach(name => moduleSet.add(statsType + '.' + name)); @@ -106,7 +215,7 @@ export function processResults( clusters[clusterUuid].module.count += stateModule.count; } - const heartbeatState = get(hit, '_source.beats_state.state.heartbeat'); + const heartbeatState = hit._source.beats_state?.state?.heartbeat; if (heartbeatState !== undefined) { if (!clusters[clusterUuid].hasOwnProperty('heartbeat')) { clusters[clusterUuid].heartbeat = { @@ -114,7 +223,7 @@ export function processResults( endpoints: 0, }; } - const clusterHb = clusters[clusterUuid].heartbeat; + const clusterHb = clusters[clusterUuid].heartbeat!; clusterHb.monitors += heartbeatState.monitors; clusterHb.endpoints += heartbeatState.endpoints; @@ -133,12 +242,12 @@ export function processResults( endpoints: 0, }; } - clusterHb[proto].monitors += val.monitors; - clusterHb[proto].endpoints += val.endpoints; + (clusterHb[proto] as HeartbeatBase).monitors += val.monitors; + (clusterHb[proto] as HeartbeatBase).endpoints += val.endpoints; } } - const functionbeatState = get(hit, '_source.beats_state.state.functionbeat'); + const functionbeatState = hit._source.beats_state?.state?.functionbeat; if (functionbeatState !== undefined) { if (!clusters[clusterUuid].hasOwnProperty('functionbeat')) { clusters[clusterUuid].functionbeat = { @@ -148,14 +257,11 @@ export function processResults( }; } - clusters[clusterUuid].functionbeat.functions.count += get( - functionbeatState, - 'functions.count', - 0 - ); + clusters[clusterUuid].functionbeat!.functions.count += + functionbeatState.functions?.count || 0; } - const stateHost = get(hit, '_source.beats_state.state.host'); + const stateHost = hit._source.beats_state?.state?.host; if (stateHost !== undefined) { const hostMap = clusterArchitectureMaps[clusterUuid]; const hostKey = `${stateHost.architecture}/${stateHost.os.platform}`; @@ -198,14 +304,14 @@ export function processResults( * @return {Promise} */ async function fetchBeatsByType( - server, - callCluster, - clusterUuids, - start, - end, - { page = 0, ...options } = {}, - type -) { + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[], + start: StatsCollectionConfig['start'], + end: StatsCollectionConfig['end'], + { page = 0, ...options }: { page?: number } & BeatsProcessOptions, + type: string +): Promise { const params = { index: INDEX_PATTERN_BEATS, ignoreUnavailable: true, @@ -232,7 +338,7 @@ async function fetchBeatsByType( { bool: { must_not: { term: { [`${type}.beat.type`]: 'apm-server' } }, - must: { term: { type: type } }, + must: { term: { type } }, }, }, ], @@ -244,8 +350,8 @@ async function fetchBeatsByType( }, }; - const results = await callCluster('search', params); - const hitsLength = get(results, 'hits.hits.length', 0); + const results = await callCluster>('search', params); + const hitsLength = results?.hits?.hits.length || 0; if (hitsLength > 0) { // further augment the clusters object with more stats processResults(results, options); @@ -265,20 +371,40 @@ async function fetchBeatsByType( return Promise.resolve(); } -export async function fetchBeatsStats(...args) { - return fetchBeatsByType(...args, 'beats_stats'); +export async function fetchBeatsStats( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[], + start: StatsCollectionConfig['start'], + end: StatsCollectionConfig['end'], + options: { page?: number } & BeatsProcessOptions +) { + return fetchBeatsByType(server, callCluster, clusterUuids, start, end, options, 'beats_stats'); } -export async function fetchBeatsStates(...args) { - return fetchBeatsByType(...args, 'beats_state'); +export async function fetchBeatsStates( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[], + start: StatsCollectionConfig['start'], + end: StatsCollectionConfig['end'], + options: { page?: number } & BeatsProcessOptions +) { + return fetchBeatsByType(server, callCluster, clusterUuids, start, end, options, 'beats_state'); } /* * Call the function for fetching and summarizing beats stats * @return {Object} - Beats stats in an object keyed by the cluster UUIDs */ -export async function getBeatsStats(server, callCluster, clusterUuids, start, end) { - const options = { +export async function getBeatsStats( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[], + start: StatsCollectionConfig['start'], + end: StatsCollectionConfig['end'] +) { + const options: BeatsProcessOptions = { clusters: {}, // the result object to be built up clusterHostSets: {}, // passed to processResults for tracking state in the results generation clusterInputSets: {}, // passed to processResults for tracking state in the results generation diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts similarity index 77% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts index 1f62c677dbb21..4f952b9dec6da 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import sinon from 'sinon'; import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse, -} from '../get_cluster_uuids'; +} from './get_cluster_uuids'; describe('get_cluster_uuids', () => { const callCluster = sinon.stub(); @@ -35,20 +34,24 @@ describe('get_cluster_uuids', () => { const expectedUuids = response.aggregations.cluster_uuids.buckets .map(bucket => bucket.key) .map(expectedUuid => ({ clusterUuid: expectedUuid })); - const start = new Date(); - const end = new Date(); + const start = new Date().toISOString(); + const end = new Date().toISOString(); describe('getClusterUuids', () => { it('returns cluster UUIDs', async () => { callCluster.withArgs('search').returns(Promise.resolve(response)); - expect(await getClusterUuids({ server, callCluster, start, end })).to.eql(expectedUuids); + expect( + await getClusterUuids({ server, callCluster, start, end, usageCollection: {} as any }) + ).toStrictEqual(expectedUuids); }); }); describe('fetchClusterUuids', () => { it('searches for clusters', async () => { callCluster.returns(Promise.resolve(response)); - expect(await fetchClusterUuids({ server, callCluster, start, end })).to.be(response); + expect( + await fetchClusterUuids({ server, callCluster, start, end, usageCollection: {} as any }) + ).toStrictEqual(response); }); }); @@ -56,12 +59,12 @@ describe('get_cluster_uuids', () => { // filterPath makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusterUuids = handleClusterUuidsResponse({}); - expect(clusterUuids.length).to.be(0); + expect(clusterUuids.length).toStrictEqual(0); }); it('handles valid response', () => { const clusterUuids = handleClusterUuidsResponse(response); - expect(clusterUuids).to.eql(expectedUuids); + expect(clusterUuids).toStrictEqual(expectedUuids); }); it('handles no buckets response', () => { @@ -73,7 +76,7 @@ describe('get_cluster_uuids', () => { }, }); - expect(clusterUuids.length).to.be(0); + expect(clusterUuids.length).toStrictEqual(0); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_es_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.test.ts similarity index 82% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_es_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.test.ts index 536e831640fad..70ed2240b47d4 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_es_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.test.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import sinon from 'sinon'; import { fetchElasticsearchStats, getElasticsearchStats, handleElasticsearchStats, -} from '../get_es_stats'; +} from './get_es_stats'; describe('get_es_stats', () => { const callWith = sinon.stub(); @@ -41,7 +40,9 @@ describe('get_es_stats', () => { it('returns clusters', async () => { callWith.withArgs('search').returns(Promise.resolve(response)); - expect(await getElasticsearchStats(server, callWith, clusterUuids)).to.eql(expectedClusters); + expect(await getElasticsearchStats(server, callWith, clusterUuids)).toStrictEqual( + expectedClusters + ); }); }); @@ -49,28 +50,28 @@ describe('get_es_stats', () => { it('searches for clusters', async () => { callWith.returns(response); - expect(await fetchElasticsearchStats(server, callWith, clusterUuids)).to.be(response); + expect(await fetchElasticsearchStats(server, callWith, clusterUuids)).toStrictEqual(response); }); }); describe('handleElasticsearchStats', () => { // filterPath makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { - const clusters = handleElasticsearchStats({}); + const clusters = handleElasticsearchStats({} as any); - expect(clusters.length).to.be(0); + expect(clusters.length).toStrictEqual(0); }); it('handles valid response', () => { - const clusters = handleElasticsearchStats(response); + const clusters = handleElasticsearchStats(response as any); - expect(clusters).to.eql(expectedClusters); + expect(clusters).toStrictEqual(expectedClusters); }); it('handles no hits response', () => { - const clusters = handleElasticsearchStats({ hits: { hits: [] } }); + const clusters = handleElasticsearchStats({ hits: { hits: [] } } as any); - expect(clusters.length).to.be(0); + expect(clusters.length).toStrictEqual(0); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts similarity index 71% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts index 52d34258b5fa4..f0ae1163d3f52 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +import { StatsCollectionConfig } from 'src/legacy/core_plugins/telemetry/server/collection_manager'; +import { SearchResponse } from 'elasticsearch'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; /** @@ -13,10 +14,14 @@ import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; * @param {Object} server The server instance * @param {function} callCluster The callWithRequest or callWithInternalUser handler * @param {Array} clusterUuids The string Cluster UUIDs to fetch details for - * @return {Promise} Array of the Elasticsearch clusters. */ -export function getElasticsearchStats(server, callCluster, clusterUuids) { - return fetchElasticsearchStats(server, callCluster, clusterUuids).then(handleElasticsearchStats); +export async function getElasticsearchStats( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[] +) { + const response = await fetchElasticsearchStats(server, callCluster, clusterUuids); + return handleElasticsearchStats(response); } /** @@ -25,9 +30,14 @@ export function getElasticsearchStats(server, callCluster, clusterUuids) { * @param {Object} server The server instance * @param {function} callCluster The callWithRequest or callWithInternalUser handler * @param {Array} clusterUuids Cluster UUIDs to limit the request against - * @return {Promise} Response for the aggregations to fetch details for the product. + * + * Returns the response for the aggregations to fetch details for the product. */ -export function fetchElasticsearchStats(server, callCluster, clusterUuids) { +export function fetchElasticsearchStats( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[] +) { const config = server.config(); const params = { index: INDEX_PATTERN_ELASTICSEARCH, @@ -67,13 +77,16 @@ export function fetchElasticsearchStats(server, callCluster, clusterUuids) { return callCluster('search', params); } +export interface ESClusterStats { + cluster_uuid: string; + type: 'cluster_stats'; +} + /** * Extract the cluster stats for each cluster. - * - * @return {Array} The Elasticsearch clusters. */ -export function handleElasticsearchStats(response) { - const clusters = get(response, 'hits.hits', []); +export function handleElasticsearchStats(response: SearchResponse) { + const clusters = response.hits?.hits || []; return clusters.map(cluster => cluster._source); } diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_high_level_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.test.ts similarity index 91% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_high_level_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.test.ts index 1c1f8dc888d01..76c80e2eb3d37 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_high_level_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.test.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import sinon from 'sinon'; import { fetchHighLevelStats, getHighLevelStats, handleHighLevelStatsResponse, -} from '../get_high_level_stats'; +} from './get_high_level_stats'; describe('get_high_level_stats', () => { const callWith = sinon.stub(); @@ -244,9 +243,9 @@ describe('get_high_level_stats', () => { it('returns clusters', async () => { callWith.withArgs('search').returns(Promise.resolve(response)); - expect(await getHighLevelStats(server, callWith, clusterUuids, start, end, product)).to.eql( - expectedClusters - ); + expect( + await getHighLevelStats(server, callWith, clusterUuids, start, end, product) + ).toStrictEqual(expectedClusters); }); }); @@ -254,30 +253,30 @@ describe('get_high_level_stats', () => { it('searches for clusters', async () => { callWith.returns(Promise.resolve(response)); - expect(await fetchHighLevelStats(server, callWith, clusterUuids, start, end, product)).to.be( - response - ); + expect( + await fetchHighLevelStats(server, callWith, clusterUuids, start, end, product) + ).toStrictEqual(response); }); }); describe('handleHighLevelStatsResponse', () => { // filterPath makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { - const clusters = handleHighLevelStatsResponse({}, product); + const clusters = handleHighLevelStatsResponse({} as any, product); - expect(clusters).to.eql({}); + expect(clusters).toStrictEqual({}); }); it('handles valid response', () => { - const clusters = handleHighLevelStatsResponse(response, product); + const clusters = handleHighLevelStatsResponse(response as any, product); - expect(clusters).to.eql(expectedClusters); + expect(clusters).toStrictEqual(expectedClusters); }); it('handles no hits response', () => { - const clusters = handleHighLevelStatsResponse({ hits: { hits: [] } }, product); + const clusters = handleHighLevelStatsResponse({ hits: { hits: [] } } as any, product); - expect(clusters).to.eql({}); + expect(clusters).toStrictEqual({}); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts similarity index 66% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts index b87f632308e4d..f67f80940d9f4 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts @@ -5,6 +5,8 @@ */ import { get } from 'lodash'; +import { StatsCollectionConfig } from 'src/legacy/core_plugins/telemetry/server/collection_manager'; +import { SearchResponse } from 'elasticsearch'; import { createQuery } from './create_query'; import { INDEX_PATTERN_KIBANA, @@ -17,13 +19,40 @@ import { TELEMETRY_QUERY_SOURCE, } from '../../common/constants'; +export interface ClusterCloudStats { + name: string; + count: number; + vms: number; + regions: Array<{ region: string; count: number }>; + vm_types: Array<{ vm_type: string; count: number }>; + zones: Array<{ zone: string; count: number }>; +} + +export interface ClusterHighLevelStats { + count: number; + versions: Array<{ version: string; count: number }>; + os: { + platforms: Array<{ platform: string; count: number }>; + platformReleases: Array<{ platformRelease: string; count: number }>; + distros: Array<{ distro: string; count: number }>; + distroReleases: Array<{ distroRelease: string; count: number }>; + }; + cloud: ClusterCloudStats[] | undefined; +} + +export interface ClustersHighLevelStats { + [clusterUuid: string]: ClusterHighLevelStats; +} + +type Counter = Map; + /** * Update a counter associated with the {@code key}. * * @param {Map} map Map to update the counter for the {@code key}. * @param {String} key The key to increment a counter for. */ -function incrementByKey(map, key) { +function incrementByKey(map: Counter, key?: string) { if (!key) { return; } @@ -37,13 +66,29 @@ function incrementByKey(map, key) { map.set(key, count + 1); } +interface InternalCloudMap { + count: number; + unique: Set; + vm_type: Counter; + region: Counter; + zone: Counter; +} + +interface CloudEntry { + id: string; + name: string; + vm_type: string; + region: string; + zone: string; +} + /** * Help to reduce Cloud metrics into unidentifiable metrics (e.g., count IDs so that they can be dropped). * * @param {Map} clouds Existing cloud data by cloud name. * @param {Object} cloud Cloud object loaded from Elasticsearch data. */ -function reduceCloudForCluster(cloudMap, cloud) { +function reduceCloudForCluster(cloudMap: Map, cloud?: CloudEntry) { if (!cloud) { return; } @@ -74,22 +119,48 @@ function reduceCloudForCluster(cloudMap, cloud) { incrementByKey(cloudByName.zone, cloud.zone); } +interface InternalClusterMap { + count: number; + versions: Counter; + cloudMap: Map; + os: { + platforms: Counter; + platformReleases: Counter; + distros: Counter; + distroReleases: Counter; + }; +} + +interface OSData { + platform?: string; + platformRelease?: string; + distro?: string; + distroRelease?: string; +} + /** * Group the instances (hits) by clusters. * * @param {Array} instances Array of hits from the request containing the cluster UUID and version. * @param {String} product The product to limit too ('kibana', 'logstash', 'beats') - * @return {Map} A map of the Cluster UUID to an {@link Object} containing the {@code count} and {@code versions} {@link Map} + * + * Returns a map of the Cluster UUID to an {@link Object} containing the {@code count} and {@code versions} {@link Map} */ -function groupInstancesByCluster(instances, product) { - const clusterMap = new Map(); +function groupInstancesByCluster( + instances: Array<{ _source: T }>, + product: string +) { + const clusterMap = new Map(); // hits are sorted arbitrarily by product UUID instances.map(instance => { - const clusterUuid = get(instance, '_source.cluster_uuid'); - const version = get(instance, `_source.${product}_stats.${product}.version`); - const cloud = get(instance, `_source.${product}_stats.cloud`); - const os = get(instance, `_source.${product}_stats.os`); + const clusterUuid = instance._source.cluster_uuid; + const version: string | undefined = get( + instance, + `_source.${product}_stats.${product}.version` + ); + const cloud: CloudEntry | undefined = get(instance, `_source.${product}_stats.cloud`); + const os: OSData | undefined = get(instance, `_source.${product}_stats.os`); if (clusterUuid) { let cluster = clusterMap.get(clusterUuid); @@ -134,16 +205,12 @@ function groupInstancesByCluster(instances, product) { * { [keyName]: key1, count: value1 }, * { [keyName]: key2, count: value2 } * ] - * - * @param {Map} map [description] - * @param {String} keyName [description] - * @return {Array} [description] */ -function mapToList(map, keyName) { - const list = []; +function mapToList(map: Map, keyName: string): T[] { + const list: T[] = []; for (const [key, count] of map) { - list.push({ [keyName]: key, count }); + list.push(({ [keyName]: key, count } as unknown) as T); } return list; @@ -154,7 +221,7 @@ function mapToList(map, keyName) { * * @param {*} product The product id, which should be in the constants file */ -function getIndexPatternForStackProduct(product) { +function getIndexPatternForStackProduct(product: string) { switch (product) { case KIBANA_SYSTEM_ID: return INDEX_PATTERN_KIBANA; @@ -176,23 +243,41 @@ function getIndexPatternForStackProduct(product) { * @param {Date} start Start time to limit the stats * @param {Date} end End time to limit the stats * @param {String} product The product to limit too ('kibana', 'logstash', 'beats') - * @return {Promise} Object keyed by the cluster UUIDs to make grouping easier. + * + * Returns an object keyed by the cluster UUIDs to make grouping easier. */ -export function getHighLevelStats(server, callCluster, clusterUuids, start, end, product) { - return fetchHighLevelStats( +export async function getHighLevelStats( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[], + start: StatsCollectionConfig['start'], + end: StatsCollectionConfig['end'], + product: string +) { + const response = await fetchHighLevelStats( server, callCluster, clusterUuids, start, end, product - ).then(response => handleHighLevelStatsResponse(response, product)); + ); + return handleHighLevelStatsResponse(response, product); } -export async function fetchHighLevelStats(server, callCluster, clusterUuids, start, end, product) { +export async function fetchHighLevelStats< + T extends { cluster_uuid?: string } = { cluster_uuid?: string } +>( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[], + start: StatsCollectionConfig['start'] | undefined, + end: StatsCollectionConfig['end'] | undefined, + product: string +): Promise> { const config = server.config(); const isKibanaIndex = product === KIBANA_SYSTEM_ID; - const filters = [{ terms: { cluster_uuid: clusterUuids } }]; + const filters: object[] = [{ terms: { cluster_uuid: clusterUuids } }]; // we should supply this from a parameter in the future so that this remains generic if (isKibanaIndex) { @@ -257,13 +342,17 @@ export async function fetchHighLevelStats(server, callCluster, clusterUuids, sta * * @param {Object} response The response from the aggregation * @param {String} product The product to limit too ('kibana', 'logstash', 'beats') - * @return {Object} Object keyed by the cluster UUIDs to make grouping easier. + * + * Returns an object keyed by the cluster UUIDs to make grouping easier. */ -export function handleHighLevelStatsResponse(response, product) { - const instances = get(response, 'hits.hits', []); +export function handleHighLevelStatsResponse( + response: SearchResponse<{ cluster_uuid?: string }>, + product: string +) { + const instances = response.hits?.hits || []; const clusterMap = groupInstancesByCluster(instances, product); - const clusters = {}; + const clusters: ClustersHighLevelStats = {}; for (const [clusterUuid, cluster] of clusterMap) { // it's unlikely this will be an array of more than one, but it is one just incase @@ -271,14 +360,15 @@ export function handleHighLevelStatsResponse(response, product) { // remap the clouds (most likely singular or empty) for (const [name, cloud] of cluster.cloudMap) { - clouds.push({ + const cloudStats: ClusterCloudStats = { name, count: cloud.count, vms: cloud.unique.size, regions: mapToList(cloud.region, 'region'), vm_types: mapToList(cloud.vm_type, 'vm_type'), zones: mapToList(cloud.zone, 'zone'), - }); + }; + clouds.push(cloudStats); } // map stats for product by cluster so that it can be joined with ES cluster stats diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_kibana_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.test.ts similarity index 79% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_kibana_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.test.ts index 98e0afa28fba3..0092e848c827b 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_kibana_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.test.ts @@ -4,19 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getUsageStats, combineStats, rollUpTotals, ensureTimeSpan } from '../get_kibana_stats'; -import expect from '@kbn/expect'; +import { + getUsageStats, + combineStats, + rollUpTotals, + ensureTimeSpan, + KibanaUsageStats, +} from './get_kibana_stats'; +import { SearchResponse } from 'elasticsearch'; describe('Get Kibana Stats', () => { describe('Make a map of usage stats for each cluster', () => { - it('passes through if there are no kibana instances', () => { - const rawStats = {}; - expect(getUsageStats(rawStats)).to.eql({}); + test('passes through if there are no kibana instances', () => { + const rawStats = {} as SearchResponse; + expect(getUsageStats(rawStats)).toStrictEqual({}); }); describe('with single cluster', () => { describe('single index', () => { - it('for a single unused instance', () => { + test('for a single unused instance', () => { const rawStats = { hits: { hits: [ @@ -39,7 +45,7 @@ describe('Get Kibana Stats', () => { }, ], }, - }; + } as any; const expected = { clusterone: { dashboard: { total: 0 }, @@ -53,10 +59,10 @@ describe('Get Kibana Stats', () => { }, }; - expect(getUsageStats(rawStats)).to.eql(expected); + expect(getUsageStats(rawStats)).toStrictEqual(expected); }); - it('for a single instance of active usage', () => { + test('for a single instance of active usage', () => { const rawStats = { hits: { hits: [ @@ -79,7 +85,7 @@ describe('Get Kibana Stats', () => { }, ], }, - }; + } as any; const expected = { clusterone: { dashboard: { total: 1 }, @@ -92,11 +98,49 @@ describe('Get Kibana Stats', () => { plugins: {}, }, }; + expect(getUsageStats(rawStats)).toStrictEqual(expected); + }); - expect(getUsageStats(rawStats)).to.eql(expected); + test('it merges the plugin stats and kibana', () => { + const rawStats = { + hits: { + hits: [ + { + _source: { + cluster_uuid: 'clusterone', + kibana_stats: { + kibana: { version: '7.0.0-alpha1-test02' }, + usage: { + dashboard: { total: 1 }, + visualization: { total: 3 }, + search: { total: 1 }, + index_pattern: { total: 1 }, + graph_workspace: { total: 1 }, + timelion_sheet: { total: 1 }, + index: '.kibana-test-01', + }, + }, + }, + }, + ], + }, + } as any; + const expected = { + clusterone: { + dashboard: { total: 1 }, + visualization: { total: 3 }, + search: { total: 1 }, + index_pattern: { total: 1 }, + graph_workspace: { total: 1 }, + timelion_sheet: { total: 1 }, + indices: 1, + plugins: {}, + }, + }; + expect(getUsageStats(rawStats)).toStrictEqual(expected); }); - it('flattens x-pack stats', () => { + test('flattens x-pack stats', () => { const rawStats = { hits: { hits: [ @@ -126,8 +170,9 @@ describe('Get Kibana Stats', () => { }, ], }, - }; - expect(getUsageStats(rawStats)).to.eql({ + } as any; + + expect(getUsageStats(rawStats)).toStrictEqual({ clusterone: { dashboard: { total: 1 }, visualization: { total: 3 }, @@ -143,7 +188,7 @@ describe('Get Kibana Stats', () => { }); describe('separate indices', () => { - it('with one unused instance', () => { + test('with one unused instance', () => { const rawStats = { hits: { hits: [ @@ -200,7 +245,7 @@ describe('Get Kibana Stats', () => { }, ], }, - }; + } as any; const expected = { clusterone: { dashboard: { total: 1 }, @@ -213,11 +258,10 @@ describe('Get Kibana Stats', () => { plugins: {}, }, }; - - expect(getUsageStats(rawStats)).to.eql(expected); + expect(getUsageStats(rawStats)).toStrictEqual(expected); }); - it('with all actively used instances', () => { + test('with all actively used instances', () => { const rawStats = { hits: { hits: [ @@ -274,7 +318,7 @@ describe('Get Kibana Stats', () => { }, ], }, - }; + } as any; const expected = { clusterone: { dashboard: { total: 4 }, @@ -287,15 +331,14 @@ describe('Get Kibana Stats', () => { plugins: {}, }, }; - - expect(getUsageStats(rawStats)).to.eql(expected); + expect(getUsageStats(rawStats)).toStrictEqual(expected); }); }); }); describe('with multiple clusters', () => { describe('separate indices', () => { - it('with all actively used instances', () => { + test('with all actively used instances', () => { const rawStats = { hits: { hits: [ @@ -369,7 +412,7 @@ describe('Get Kibana Stats', () => { }, ], }, - }; + } as any; const expected = { clusterone: { dashboard: { total: 4 }, @@ -392,29 +435,28 @@ describe('Get Kibana Stats', () => { plugins: {}, }, }; - - expect(getUsageStats(rawStats)).to.eql(expected); + expect(getUsageStats(rawStats)).toStrictEqual(expected); }); }); }); }); describe('Combines usage stats with high-level stats', () => { - it('passes through if there are no kibana instances', () => { + test('passes through if there are no kibana instances', () => { const highLevelStats = {}; const usageStats = {}; - expect(combineStats(highLevelStats, usageStats)).to.eql({}); + expect(combineStats(highLevelStats, usageStats)).toStrictEqual({}); }); describe('adds usage stats to high-level stats', () => { - it('for a single cluster', () => { + test('for a single cluster', () => { const highLevelStats = { clusterone: { count: 2, versions: [{ count: 2, version: '7.0.0-alpha1-test12' }], }, - }; + } as any; const usageStats = { clusterone: { dashboard: { total: 1 }, @@ -428,7 +470,7 @@ describe('Get Kibana Stats', () => { }, }; - expect(combineStats(highLevelStats, usageStats)).to.eql({ + expect(combineStats(highLevelStats, usageStats)).toStrictEqual({ clusterone: { count: 2, dashboard: { total: 1 }, @@ -444,7 +486,7 @@ describe('Get Kibana Stats', () => { }); }); - it('for multiple single clusters', () => { + test('for multiple single clusters', () => { const highLevelStats = { clusterone: { count: 2, @@ -454,7 +496,7 @@ describe('Get Kibana Stats', () => { count: 1, versions: [{ count: 1, version: '7.0.0-alpha1-test14' }], }, - }; + } as any; const usageStats = { clusterone: { dashboard: { total: 1 }, @@ -478,7 +520,7 @@ describe('Get Kibana Stats', () => { }, }; - expect(combineStats(highLevelStats, usageStats)).to.eql({ + expect(combineStats(highLevelStats, usageStats)).toStrictEqual({ clusterone: { count: 2, dashboard: { total: 1 }, @@ -508,16 +550,16 @@ describe('Get Kibana Stats', () => { }); describe('if usage stats are empty', () => { - it('returns just high-level stats', () => { + test('returns just high-level stats', () => { const highLevelStats = { clusterone: { count: 2, versions: [{ count: 2, version: '7.0.0-alpha1-test12' }], }, - }; + } as any; const usageStats = undefined; - expect(combineStats(highLevelStats, usageStats)).to.eql({ + expect(combineStats(highLevelStats, usageStats)).toStrictEqual({ clusterone: { count: 2, versions: [{ count: 2, version: '7.0.0-alpha1-test12' }], @@ -528,64 +570,64 @@ describe('Get Kibana Stats', () => { }); describe('Rolls up stats when there are multiple Kibana indices for a cluster', () => { - it('by combining the `total` fields where previous was 0', () => { - const rollUp = { my_field: { total: 0 } }; + test('by combining the `total` fields where previous was 0', () => { + const rollUp = { my_field: { total: 0 } } as any; const addOn = { my_field: { total: 1 } }; - expect(rollUpTotals(rollUp, addOn, 'my_field')).to.eql({ total: 1 }); + expect(rollUpTotals(rollUp, addOn, 'my_field' as any)).toStrictEqual({ total: 1 }); }); - it('by combining the `total` fields with > 1 for previous and addOn', () => { - const rollUp = { my_field: { total: 1 } }; + test('by combining the `total` fields with > 1 for previous and addOn', () => { + const rollUp = { my_field: { total: 1 } } as any; const addOn = { my_field: { total: 3 } }; - expect(rollUpTotals(rollUp, addOn, 'my_field')).to.eql({ total: 4 }); + expect(rollUpTotals(rollUp, addOn, 'my_field' as any)).toStrictEqual({ total: 4 }); }); }); describe('Ensure minimum time difference', () => { - it('should return start and end as is when none are provided', () => { + test('should return start and end as is when none are provided', () => { const { start, end } = ensureTimeSpan(undefined, undefined); - expect(start).to.be.undefined; - expect(end).to.be.undefined; + expect(start).toBe(undefined); + expect(end).toBe(undefined); }); - it('should return start and end as is when only end is provided', () => { + test('should return start and end as is when only end is provided', () => { const initialEnd = '2020-01-01T00:00:00Z'; const { start, end } = ensureTimeSpan(undefined, initialEnd); - expect(start).to.be.undefined; - expect(end).to.be.equal(initialEnd); + expect(start).toBe(undefined); + expect(end).toEqual(initialEnd); }); - it('should return start and end as is because they are already 24h away', () => { + test('should return start and end as is because they are already 24h away', () => { const initialStart = '2019-12-31T00:00:00Z'; const initialEnd = '2020-01-01T00:00:00Z'; const { start, end } = ensureTimeSpan(initialStart, initialEnd); - expect(start).to.be.equal(initialStart); - expect(end).to.be.equal(initialEnd); + expect(start).toEqual(initialStart); + expect(end).toEqual(initialEnd); }); - it('should return start and end as is because they are already 24h+ away', () => { + test('should return start and end as is because they are already 24h+ away', () => { const initialStart = '2019-12-31T00:00:00Z'; const initialEnd = '2020-01-01T01:00:00Z'; const { start, end } = ensureTimeSpan(initialStart, initialEnd); - expect(start).to.be.equal(initialStart); - expect(end).to.be.equal(initialEnd); + expect(start).toEqual(initialStart); + expect(end).toEqual(initialEnd); }); - it('should modify start to a date 24h before end', () => { + test('should modify start to a date 24h before end', () => { const initialStart = '2020-01-01T00:00:00.000Z'; const initialEnd = '2020-01-01T01:00:00.000Z'; const { start, end } = ensureTimeSpan(initialStart, initialEnd); - expect(start).to.be.equal('2019-12-31T01:00:00.000Z'); - expect(end).to.be.equal(initialEnd); + expect(start).toEqual('2019-12-31T01:00:00.000Z'); + expect(end).toEqual(initialEnd); }); - it('should modify start to a date 24h before now', () => { + test('should modify start to a date 24h before now', () => { const initialStart = new Date().toISOString(); const { start, end } = ensureTimeSpan(initialStart, undefined); - expect(start).to.not.be.equal(initialStart); - expect(end).to.be.undefined; + expect(start).not.toBe(initialStart); + expect(end).toBe(undefined); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts similarity index 58% rename from x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts index 1e22507c5baf4..e2ad64ce04c6b 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.ts @@ -5,30 +5,78 @@ */ import moment from 'moment'; -import { get, isEmpty, omit } from 'lodash'; +import { isEmpty } from 'lodash'; +import { StatsCollectionConfig } from 'src/legacy/core_plugins/telemetry/server/collection_manager'; +import { SearchResponse } from 'elasticsearch'; import { KIBANA_SYSTEM_ID, TELEMETRY_COLLECTION_INTERVAL } from '../../common/constants'; -import { fetchHighLevelStats, handleHighLevelStatsResponse } from './get_high_level_stats'; +import { + fetchHighLevelStats, + handleHighLevelStatsResponse, + ClustersHighLevelStats, + ClusterHighLevelStats, +} from './get_high_level_stats'; -export function rollUpTotals(rolledUp, addOn, field) { - const rolledUpTotal = get(rolledUp, [field, 'total'], 0); - const addOnTotal = get(addOn, [field, 'total'], 0); +export function rollUpTotals( + rolledUp: ClusterUsageStats, + addOn: { [key: string]: { total?: number } | undefined }, + field: Exclude +) { + const rolledUpTotal = rolledUp[field]?.total || 0; + const addOnTotal = addOn[field]?.total || 0; return { total: rolledUpTotal + addOnTotal }; } -export function rollUpIndices(rolledUp) { +export function rollUpIndices(rolledUp: ClusterUsageStats) { return rolledUp.indices + 1; } +export interface KibanaUsageStats { + cluster_uuid: string; + kibana_stats?: { + usage?: { + index?: string; + } & { + [plugin: string]: { + total: number; + }; + }; + }; +} + +export interface ClusterUsageStats { + dashboard?: { total: number }; + visualization?: { total: number }; + search?: { total: number }; + index_pattern?: { total: number }; + graph_workspace?: { total: number }; + timelion_sheet?: { total: number }; + indices: number; + plugins?: { + xpack?: unknown; + [plugin: string]: unknown; + }; +} + +export interface ClustersUsageStats { + [clusterUuid: string]: ClusterUsageStats | undefined; +} + +export interface KibanaClusterStat extends Partial, ClusterHighLevelStats {} + +export interface KibanaStats { + [clusterUuid: string]: KibanaClusterStat; +} + /* * @param {Object} rawStats */ -export function getUsageStats(rawStats) { +export function getUsageStats(rawStats: SearchResponse) { const clusterIndexCache = new Set(); - const rawStatsHits = get(rawStats, 'hits.hits', []); + const rawStatsHits = rawStats.hits?.hits || []; // get usage stats per cluster / .kibana index return rawStatsHits.reduce((accum, currInstance) => { - const clusterUuid = get(currInstance, '_source.cluster_uuid'); - const currUsage = get(currInstance, '_source.kibana_stats.usage', {}); + const clusterUuid = currInstance._source.cluster_uuid; + const currUsage = currInstance._source.kibana_stats?.usage || {}; const clusterIndexCombination = clusterUuid + currUsage.index; // return early if usage data is empty or if this cluster/index has already been processed @@ -39,7 +87,7 @@ export function getUsageStats(rawStats) { // Get the stats that were read from any number of different .kibana indices in the cluster, // roll them up into cluster-wide totals - const rolledUpStats = get(accum, clusterUuid, { indices: 0 }); + const rolledUpStats = accum[clusterUuid] || { indices: 0 }; const stats = { dashboard: rollUpTotals(rolledUpStats, currUsage, 'dashboard'), visualization: rollUpTotals(rolledUpStats, currUsage, 'visualization'), @@ -51,21 +99,22 @@ export function getUsageStats(rawStats) { }; // Get the stats provided by telemetry collectors. - const pluginsNested = omit(currUsage, [ - 'index', - 'dashboard', - 'visualization', - 'search', - 'index_pattern', - 'graph_workspace', - 'timelion_sheet', - ]); + const { + index, + dashboard, + visualization, + search, + index_pattern, + graph_workspace, + timelion_sheet, + xpack, + ...pluginsTop + } = currUsage; // Stats filtered by telemetry collectors need to be flattened since they're pulled in a generic way. // A plugin might not provide flat stats if it implements formatForBulkUpload in its collector. // e.g: we want `xpack.reporting` to just be `reporting` - const top = omit(pluginsNested, 'xpack'); - const plugins = { ...top, ...pluginsNested.xpack }; + const plugins = { ...pluginsTop, ...xpack }; return { ...accum, @@ -74,10 +123,13 @@ export function getUsageStats(rawStats) { plugins, }, }; - }, {}); + }, {} as ClustersUsageStats); } -export function combineStats(highLevelStats, usageStats = {}) { +export function combineStats( + highLevelStats: ClustersHighLevelStats, + usageStats: ClustersUsageStats = {} +) { return Object.keys(highLevelStats).reduce((accum, currClusterUuid) => { return { ...accum, @@ -86,7 +138,7 @@ export function combineStats(highLevelStats, usageStats = {}) { ...usageStats[currClusterUuid], }, }; - }, {}); + }, {} as KibanaStats); } /** @@ -96,7 +148,10 @@ export function combineStats(highLevelStats, usageStats = {}) { * @param {date} [start] The start time from which to get the telemetry data * @param {date} [end] The end time from which to get the telemetry data */ -export function ensureTimeSpan(start, end) { +export function ensureTimeSpan( + start?: StatsCollectionConfig['start'], + end?: StatsCollectionConfig['end'] +) { // We only care if we have a start date, because that's the limit that might make us lose the document if (start) { const duration = moment.duration(TELEMETRY_COLLECTION_INTERVAL, 'milliseconds'); @@ -117,9 +172,15 @@ export function ensureTimeSpan(start, end) { * Monkey-patch the modules from get_high_level_stats and add in the * specialized usage data that comes with kibana stats (kibana_stats.usage). */ -export async function getKibanaStats(server, callCluster, clusterUuids, start, end) { +export async function getKibanaStats( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[], + start: StatsCollectionConfig['start'], + end: StatsCollectionConfig['end'] +) { const { start: safeStart, end: safeEnd } = ensureTimeSpan(start, end); - const rawStats = await fetchHighLevelStats( + const rawStats = await fetchHighLevelStats( server, callCluster, clusterUuids, diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts index 49a925d1dad0b..f0fda5229cb5c 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts @@ -5,7 +5,6 @@ */ import { telemetryCollectionManager } from '../../../../../../src/legacy/core_plugins/telemetry/server'; -// @ts-ignore import { getAllStats } from './get_all_stats'; import { getClusterUuids } from './get_cluster_uuids'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index eb76498b55f4c..da1760d3773d4 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -57,7 +57,7 @@ exports[`CheckupTab render with deprecations 1`] = ` - @@ -143,7 +143,6 @@ export class CheckupTab extends UpgradeAssistantTabComponent void; currentFilter: LevelFilterOption; onFilterChange: (filter: LevelFilterOption) => void; - search: string; onSearchChange: (filter: string) => void; availableGroupByOptions: GroupByOption[]; currentGroupBy: GroupByOption; onGroupByChange: (groupBy: GroupByOption) => void; } -export const CheckupControlsUI: FunctionComponent = ({ +export const CheckupControls: FunctionComponent = ({ allDeprecations, loadingState, loadData, currentFilter, onFilterChange, - search, onSearchChange, availableGroupByOptions, currentGroupBy, onGroupByChange, - intl, -}) => ( - - - onSearchChange(e.target.value)} - /> - - - {/* These two components provide their own EuiFlexItem wrappers */} - - +}) => { + const [searchTermError, setSearchTermError] = useState(null); + const filterInvalid = Boolean(searchTermError); + return ( + + + + + { + const string = e.target.value; + const errorMessage = validateRegExpString(string); + if (errorMessage) { + // Emit an empty search term to listeners if search term is invalid. + onSearchChange(''); + setSearchTermError(errorMessage); + } else { + onSearchChange(e.target.value); + if (searchTermError) { + setSearchTermError(null); + } + } + }} + /> + - - - - - - -); + {/* These two components provide their own EuiFlexItem wrappers */} + + -export const CheckupControls = injectI18n(CheckupControlsUI); + + + + + + + + {filterInvalid && ( + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.test.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.test.ts new file mode 100644 index 0000000000000..067f11798e151 --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { validateRegExpString } from './utils'; + +describe('validRegExpString', () => { + it('correctly returns false for invalid strings', () => { + expect(validateRegExpString('?asd')).toContain(`Invalid regular expression`); + expect(validateRegExpString('*asd')).toContain(`Invalid regular expression`); + expect(validateRegExpString('(')).toContain(`Invalid regular expression`); + }); + + it('correctly returns true for valid strings', () => { + expect(validateRegExpString('asd')).toBe(''); + expect(validateRegExpString('.*asd')).toBe(''); + expect(validateRegExpString('')).toBe(''); + }); +}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.ts new file mode 100644 index 0000000000000..6a1f32fe8f20b --- /dev/null +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.ts @@ -0,0 +1,19 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { tryCatch, fold } from 'fp-ts/lib/Either'; + +export const validateRegExpString = (s: string) => + pipe( + tryCatch( + () => new RegExp(s), + e => (e as Error).message + ), + fold( + (errorMessage: string) => errorMessage, + () => '' + ) + ); diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/index.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/index.ts new file mode 100644 index 0000000000000..ae3c9962ecadb --- /dev/null +++ b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { installXJsonMode, XJsonMode } from './x_json'; diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts new file mode 100644 index 0000000000000..ae3c9962ecadb --- /dev/null +++ b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { installXJsonMode, XJsonMode } from './x_json'; diff --git a/x-pack/plugins/searchprofiler/public/application/editor/worker/index.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts similarity index 100% rename from x-pack/plugins/searchprofiler/public/application/editor/worker/index.ts rename to x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/index.ts diff --git a/x-pack/plugins/searchprofiler/public/application/editor/worker/worker.d.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/worker.d.ts similarity index 100% rename from x-pack/plugins/searchprofiler/public/application/editor/worker/worker.d.ts rename to x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/worker.d.ts diff --git a/x-pack/plugins/searchprofiler/public/application/editor/worker/worker.js b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/worker.js similarity index 100% rename from x-pack/plugins/searchprofiler/public/application/editor/worker/worker.js rename to x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/worker/worker.js diff --git a/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json.ts b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json.ts new file mode 100644 index 0000000000000..bfeca045bea02 --- /dev/null +++ b/x-pack/plugins/es_ui_shared/console_lang/ace/modes/x_json/x_json.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ace from 'brace'; +import { XJsonMode } from '../../../../../../../src/plugins/es_ui_shared/console_lang'; +import { workerModule } from './worker'; +const { WorkerClient } = ace.acequire('ace/worker/worker_client'); + +// Then clobber `createWorker` method to install our worker source. Per ace's wiki: https://github.com/ajaxorg/ace/wiki/Syntax-validation +(XJsonMode.prototype as any).createWorker = function(session: ace.IEditSession) { + const xJsonWorker = new WorkerClient(['ace'], workerModule, 'JsonWorker'); + + xJsonWorker.attachToDocument(session.getDocument()); + + xJsonWorker.on('annotate', function(e: { data: any }) { + session.setAnnotations(e.data); + }); + + xJsonWorker.on('terminate', function() { + session.clearAnnotations(); + }); + + return xJsonWorker; +}; + +export { XJsonMode }; + +export function installXJsonMode(editor: ace.Editor) { + const session = editor.getSession(); + session.setMode(new (XJsonMode as any)()); +} diff --git a/x-pack/plugins/es_ui_shared/console_lang/index.ts b/x-pack/plugins/es_ui_shared/console_lang/index.ts new file mode 100644 index 0000000000000..b5fe3a554e34d --- /dev/null +++ b/x-pack/plugins/es_ui_shared/console_lang/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { XJsonMode, installXJsonMode } from './ace/modes'; diff --git a/x-pack/plugins/es_ui_shared/console_lang/mocks.ts b/x-pack/plugins/es_ui_shared/console_lang/mocks.ts new file mode 100644 index 0000000000000..68480282ddc03 --- /dev/null +++ b/x-pack/plugins/es_ui_shared/console_lang/mocks.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('./ace/modes/x_json/worker', () => ({ + workerModule: { id: 'ace/mode/json_worker', src: '' }, +})); diff --git a/x-pack/plugins/observability/public/components/action_menu.tsx b/x-pack/plugins/observability/public/components/action_menu.tsx index 6e964dde3aecf..a5f59ecd5506f 100644 --- a/x-pack/plugins/observability/public/components/action_menu.tsx +++ b/x-pack/plugins/observability/public/components/action_menu.tsx @@ -16,6 +16,7 @@ import { import React, { HTMLAttributes } from 'react'; import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; +import styled from 'styled-components'; type Props = EuiPopoverProps & HTMLAttributes; @@ -45,7 +46,12 @@ export const SectionLinks: React.FC<{}> = props => ( export const SectionSpacer: React.FC<{}> = () => ; -export const Section: React.FC<{}> = props => <>{props.children}; +export const Section = styled.div` + margin-bottom: 24px; + &:last-of-type { + margin-bottom: 0; + } +`; export type SectionLinkProps = EuiListGroupItemProps; export const SectionLink: React.FC = props => ( diff --git a/x-pack/plugins/searchprofiler/public/application/containers/main/main.tsx b/x-pack/plugins/searchprofiler/public/application/containers/main/main.tsx index 90617ba1c5167..aa6c20aa6a7f3 100644 --- a/x-pack/plugins/searchprofiler/public/application/containers/main/main.tsx +++ b/x-pack/plugins/searchprofiler/public/application/containers/main/main.tsx @@ -5,7 +5,6 @@ */ import React, { useCallback } from 'react'; -import _ from 'lodash'; import { EuiPage, diff --git a/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx b/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx index a70d70f117edb..ad56b3957eb74 100644 --- a/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx +++ b/x-pack/plugins/searchprofiler/public/application/editor/editor.test.tsx @@ -6,9 +6,7 @@ import 'brace'; import 'brace/mode/json'; -jest.mock('./worker', () => { - return { workerModule: { id: 'ace/mode/json_worker', src: '' } }; -}); +import '../../../../es_ui_shared/console_lang/mocks'; import { registerTestBed } from '../../../../../test_utils'; import { Editor, Props } from '.'; diff --git a/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts b/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts index e5aad16bc4af2..6f19ce12eb639 100644 --- a/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts +++ b/x-pack/plugins/searchprofiler/public/application/editor/init_editor.ts @@ -5,7 +5,7 @@ */ import ace from 'brace'; -import { installXJsonMode } from './x_json_mode'; +import { installXJsonMode } from '../../../../es_ui_shared/console_lang'; export function initializeEditor({ el, diff --git a/x-pack/plugins/searchprofiler/public/application/editor/x_json_highlight_rules.ts b/x-pack/plugins/searchprofiler/public/application/editor/x_json_highlight_rules.ts deleted file mode 100644 index 1e6e774db2e52..0000000000000 --- a/x-pack/plugins/searchprofiler/public/application/editor/x_json_highlight_rules.ts +++ /dev/null @@ -1,139 +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 ace from 'brace'; -const oop = ace.acequire('ace/lib/oop'); -const { JsonHighlightRules } = ace.acequire('ace/mode/json_highlight_rules'); -const { TextHighlightRules } = ace.acequire('ace/mode/text_highlight_rules'); - -/* - * The rules below were copied from ./src/legacy/core_plugins/console/public/src/sense_editor/mode/x_json_highlight_rules.js - * - * It is very likely that this code will move (or be removed) in future but for now - * it enables syntax highlight for extended json. - */ - -const xJsonRules = { - start: [ - { - token: [ - 'variable', - 'whitespace', - 'ace.punctuation.colon', - 'whitespace', - 'punctuation.start_triple_quote', - ], - regex: '("(?:[^"]*_)?script"|"inline"|"source")(\\s*?)(:)(\\s*?)(""")', - next: 'script-start', - merge: false, - push: true, - }, - { - token: 'variable', // single line - regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]\\s*(?=:)', - }, - { - token: 'punctuation.start_triple_quote', - regex: '"""', - next: 'string_literal', - merge: false, - push: true, - }, - { - token: 'string', // single line - regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]', - }, - { - token: 'constant.numeric', // hex - regex: '0[xX][0-9a-fA-F]+\\b', - }, - { - token: 'constant.numeric', // float - regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b', - }, - { - token: 'constant.language.boolean', - regex: '(?:true|false)\\b', - }, - { - token: 'invalid.illegal', // single quoted strings are not allowed - regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']", - }, - { - token: 'invalid.illegal', // comments are not allowed - regex: '\\/\\/.*$', - }, - { - token: 'paren.lparen', - merge: false, - regex: '{', - next: 'start', - push: true, - }, - { - token: 'paren.lparen', - merge: false, - regex: '[[(]', - }, - { - token: 'paren.rparen', - merge: false, - regex: '[\\])]', - }, - { - token: 'paren.rparen', - regex: '}', - merge: false, - next: 'pop', - }, - { - token: 'punctuation.comma', - regex: ',', - }, - { - token: 'punctuation.colon', - regex: ':', - }, - { - token: 'whitespace', - regex: '\\s+', - }, - { - token: 'text', - regex: '.+?', - }, - ], - string_literal: [ - { - token: 'punctuation.end_triple_quote', - regex: '"""', - next: 'pop', - }, - { - token: 'multi_string', - regex: '.', - }, - ], -}; - -function XJsonHighlightRules(this: any) { - this.$rules = xJsonRules; -} - -oop.inherits(XJsonHighlightRules, JsonHighlightRules); - -export function getRules() { - const ruleset: any = new (XJsonHighlightRules as any)(); - ruleset.embedRules(TextHighlightRules, 'text-', [ - { - token: 'punctuation.end_triple_quote', - regex: '"""', - next: 'pop', - }, - ]); - ruleset.normalizeRules(); - return ruleset.getRules(); -} diff --git a/x-pack/plugins/searchprofiler/public/application/editor/x_json_mode.ts b/x-pack/plugins/searchprofiler/public/application/editor/x_json_mode.ts deleted file mode 100644 index 7ac1e6105d97f..0000000000000 --- a/x-pack/plugins/searchprofiler/public/application/editor/x_json_mode.ts +++ /dev/null @@ -1,54 +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. - */ - -// Copied and modified from src/legacy/core_plugins/console/public/src/sense_editor/mode/input.js - -import ace from 'brace'; - -import * as xJsonRules from './x_json_highlight_rules'; -import { workerModule } from './worker'; - -const oop = ace.acequire('ace/lib/oop'); -const { Mode: JSONMode } = ace.acequire('ace/mode/json'); -const { Tokenizer: AceTokenizer } = ace.acequire('ace/tokenizer'); -const { MatchingBraceOutdent } = ace.acequire('ace/mode/matching_brace_outdent'); -const { CstyleBehaviour } = ace.acequire('ace/mode/behaviour/cstyle'); -const { FoldMode: CStyleFoldMode } = ace.acequire('ace/mode/folding/cstyle'); -const { WorkerClient } = ace.acequire('ace/worker/worker_client'); - -function XJsonMode(this: any) { - this.$tokenizer = new AceTokenizer(xJsonRules.getRules()); - this.$outdent = new MatchingBraceOutdent(); - this.$behaviour = new CstyleBehaviour(); - this.foldingRules = new CStyleFoldMode(); -} - -// Order here matters here: - -// 1. We first inherit -oop.inherits(XJsonMode, JSONMode); - -// 2. Then clobber `createWorker` method to install our worker source. Per ace's wiki: https://github.com/ajaxorg/ace/wiki/Syntax-validation -XJsonMode.prototype.createWorker = function(session: ace.IEditSession) { - const xJsonWorker = new WorkerClient(['ace'], workerModule, 'JsonWorker'); - - xJsonWorker.attachToDocument(session.getDocument()); - - xJsonWorker.on('annotate', function(e: { data: any }) { - session.setAnnotations(e.data); - }); - - xJsonWorker.on('terminate', function() { - session.clearAnnotations(); - }); - - return xJsonWorker; -}; - -export function installXJsonMode(editor: ace.Editor) { - const session = editor.getSession(); - session.setMode(new (XJsonMode as any)()); -} diff --git a/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts b/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts index 4267fd0d2f901..99687de0f1440 100644 --- a/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts +++ b/x-pack/plugins/searchprofiler/public/application/utils/check_for_json_errors.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Convert triple quotes into regular quotes and escape internal quotes. -function collapseLiteralStrings(data: string) { - return data.replace(/"""(?:\s*\r?\n)?((?:.|\r?\n)*?)(?:\r?\n\s*)?"""/g, function(match, literal) { - return JSON.stringify(literal); - }); -} +import { collapseLiteralStrings } from '../../../../../../src/plugins/es_ui_shared/console_lang/lib'; export function checkForParseErrors(json: string) { const sanitizedJson = collapseLiteralStrings(json); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8a542b926db6d..bb488c0a7b1fb 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3945,7 +3945,6 @@ "xpack.apm.tracesTable.tracesPerMinuteColumnLabel": "1 分あたりのトレース", "xpack.apm.tracesTable.tracesPerMinuteUnitLabel": "1分あたりトランザクション数", "xpack.apm.transactionActionMenu.actionsButtonLabel": "アクション", - "xpack.apm.transactionActionMenu.actionsLabel": "アクション", "xpack.apm.transactionActionMenu.showContainerLogsLinkLabel": "コンテナーログを表示", "xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel": "コンテナーメトリックを表示", "xpack.apm.transactionActionMenu.showHostLogsLinkLabel": "ホストログを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e5d9f9a4e2d35..402ddd8288830 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3945,7 +3945,6 @@ "xpack.apm.tracesTable.tracesPerMinuteColumnLabel": "每分钟追溯次数", "xpack.apm.tracesTable.tracesPerMinuteUnitLabel": "tpm", "xpack.apm.transactionActionMenu.actionsButtonLabel": "操作", - "xpack.apm.transactionActionMenu.actionsLabel": "操作", "xpack.apm.transactionActionMenu.showContainerLogsLinkLabel": "显示容器日志", "xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel": "显示容器指标", "xpack.apm.transactionActionMenu.showHostLogsLinkLabel": "显示主机日志", diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index 26be3421b534e..93b8cce153d3b 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import '../../../es_ui_shared/console_lang/mocks'; + import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 431eb1cae0608..943233d3c14ed 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import '../../../es_ui_shared/console_lang/mocks'; + import React from 'react'; import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index 545bfbdf7cbc2..51285a5786b00 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import '../../../es_ui_shared/console_lang/mocks'; + import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts index a0327c6dfa1db..3370e42b2417f 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_list.test.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import '../../../es_ui_shared/console_lang/mocks'; import { act } from 'react-dom/test-utils'; import * as fixtures from '../../test/fixtures'; diff --git a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts index 973c14893f342..20b7b526705c0 100644 --- a/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/plugins/watcher/__jest__/client_integration/watch_status.test.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import '../../../es_ui_shared/console_lang/mocks'; + import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { WatchStatusTestBed } from './helpers/watch_status.helpers'; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx index 91185ac604b34..b3a09d3bc0e65 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; + import { serializeJsonWatch } from '../../../../../../common/lib/serialization'; import { ErrableFormRow, SectionError, Error as ServerError } from '../../../../components'; import { onWatchSave } from '../../watch_edit_actions'; @@ -28,6 +29,8 @@ import { goToWatchList } from '../../../../lib/navigation'; import { RequestFlyout } from '../request_flyout'; import { useAppContext } from '../../../../app_context'; +import { useXJsonMode } from './use_x_json_mode'; + export const JsonWatchEditForm = () => { const { links: { putWatchApiUrl }, @@ -35,6 +38,7 @@ export const JsonWatchEditForm = () => { } = useAppContext(); const { watch, setWatchProperty } = useContext(WatchContext); + const { xJsonMode, convertToJson, setXJson, xJson } = useXJsonMode(watch.watchString); const { errors } = watch.validate(); const hasErrors = !!Object.keys(errors).find(errorKey => errors[errorKey].length >= 1); @@ -160,9 +164,9 @@ export const JsonWatchEditForm = () => { errors={jsonErrors} > { defaultMessage: 'Code editor', } )} - value={watch.watchString} - onChange={(json: string) => { + value={xJson} + onChange={(xjson: string) => { if (validationError) { setValidationError(null); } - setWatchProperty('watchString', json); + setXJson(xjson); + // Keep the watch in sync with the editor content + setWatchProperty('watchString', convertToJson(xjson)); }} /> diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx index 9ac80ade89daa..c906d05be64be 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx @@ -40,6 +40,8 @@ import { JsonWatchEditSimulateResults } from './json_watch_edit_simulate_results import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label'; import { useAppContext } from '../../../../app_context'; +import { useXJsonMode } from './use_x_json_mode'; + const actionModeOptions = Object.keys(ACTION_MODES).map(mode => ({ text: ACTION_MODES[mode], value: ACTION_MODES[mode], @@ -94,6 +96,8 @@ export const JsonWatchEditSimulate = ({ ignoreCondition, } = executeDetails; + const { setXJson, convertToJson, xJsonMode, xJson } = useXJsonMode(alternativeInput); + const columns = [ { field: 'actionId', @@ -371,22 +375,23 @@ export const JsonWatchEditSimulate = ({ > { + value={xJson} + onChange={(xjson: string) => { + setXJson(xjson); setExecuteDetails( new ExecuteDetails({ ...executeDetails, - alternativeInput: json, + alternativeInput: convertToJson(xjson), }) ); }} diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/use_x_json_mode.ts b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/use_x_json_mode.ts new file mode 100644 index 0000000000000..7aefb0554e0e8 --- /dev/null +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/use_x_json_mode.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useState } from 'react'; +import { XJsonMode } from '../../../../../../../es_ui_shared/console_lang'; +import { + collapseLiteralStrings, + expandLiteralStrings, +} from '../../../../../../../../../src/plugins/es_ui_shared/console_lang/lib'; + +export const xJsonMode = new XJsonMode(); + +export const useXJsonMode = (json: string) => { + const [xJson, setXJson] = useState(expandLiteralStrings(json)); + + return { + xJson, + setXJson, + xJsonMode, + convertToJson: collapseLiteralStrings, + }; +}; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx index c1ebcdc262863..b79f9eee01834 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx @@ -245,7 +245,7 @@ export const WebhookActionFields: React.FunctionComponent = ({ mode="json" width="100%" height="200px" - theme="github" + theme="textmate" data-test-subj="webhookBodyEditor" aria-label={i18n.translate( 'xpack.watcher.sections.watchEdit.threshold.webhookAction.bodyCodeEditorAriaLabel',