From a5805cec7ac6ecc7a782f1d44536bda91eed2f9f Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 5 May 2020 09:33:09 -0700 Subject: [PATCH] Disallows commonjs in new platform public (#65218) Signed-off-by: Tyler Smalley --- .eslintrc.js | 10 + .../editor/legacy/console_editor/editor.tsx | 6 +- .../application/containers/settings.tsx | 8 +- .../use_send_current_request_to_es.ts | 4 +- .../__tests__/output_tokenization.test.js | 2 +- .../models/legacy_core_editor/mode/input.js | 7 +- .../mode/input_highlight_rules.js | 2 +- .../models/legacy_core_editor/mode/output.js | 6 +- .../mode/output_highlight_rules.js | 2 +- .../legacy_core_editor/theme_sense_dark.js | 2 +- .../__tests__/integration.test.js | 6 +- .../__tests__/sense_editor.test.js | 2 +- .../__jest__/url_autocomplete.test.js | 2 +- .../autocomplete/__jest__/url_params.test.js | 2 +- .../public/lib/autocomplete/body_completer.js | 2 +- .../field_autocomplete_component.js | 4 +- .../index_autocomplete_component.js | 4 +- .../template_autocomplete_component.js | 4 +- .../components/type_autocomplete_component.js | 4 +- .../username_autocomplete_component.js | 4 +- .../console/public/lib/autocomplete/engine.js | 2 +- .../public/lib/autocomplete/url_params.js | 2 +- .../__tests__/curl_parsing.test.js | 10 +- .../public/lib/kb/__tests__/kb.test.js | 4 +- src/plugins/console/public/lib/kb/api.js | 2 +- .../lib/mappings/__tests__/mapping.test.js | 2 +- .../console/public/lib/mappings/mappings.js | 39 +- .../public/lib/utils/__tests__/utils.test.js | 2 +- .../plugins/xpack_main/jquery_flot.js | 11 +- .../fixtures/follower_index.ts | 2 +- .../util/geo_json_clean_and_validate.test.js | 2 +- .../file_upload/public/util/pattern_reader.js | 2 +- .../public/angular/graph_client_workspace.js | 2739 ++++++++--------- .../angular/graph_client_workspace.test.js | 4 +- x-pack/plugins/graph/public/app.js | 4 +- .../calendars/edit/import_modal/utils.js | 2 +- 36 files changed, 1450 insertions(+), 1462 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4511ecbe223b80..da7040fd5a991f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -157,6 +157,16 @@ module.exports = { }, }, + /** + * New Platform client-side + */ + { + files: ['{src,x-pack}/plugins/*/public/**/*.{js,ts,tsx}'], + rules: { + 'import/no-commonjs': 'error', + }, + }, + /** * Files that require Elastic license headers instead of Apache 2.0 header */ diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index cf62de82bcf4b3..47f7aa16352053 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -24,7 +24,7 @@ import { parse } from 'query-string'; import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { useUIAceKeyboardMode } from '../../../../../../../es_ui_shared/public'; // @ts-ignore -import mappings from '../../../../../lib/mappings/mappings'; +import { retrieveAutoCompleteInfo, clearSubscriptions } from '../../../../../lib/mappings/mappings'; import { ConsoleMenu } from '../../../../components'; import { useEditorReadContext, useServicesContext } from '../../../../contexts'; import { @@ -172,14 +172,14 @@ function EditorUI({ initialTextValue }: EditorProps) { setInputEditor(editor); setTextArea(editorRef.current!.querySelector('textarea')); - mappings.retrieveAutoCompleteInfo(settingsService, settingsService.getAutocomplete()); + retrieveAutoCompleteInfo(settingsService, settingsService.getAutocomplete()); const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor); setupAutosave(); return () => { unsubscribeResizer(); - mappings.clearSubscriptions(); + clearSubscriptions(); window.removeEventListener('hashchange', onHashChange); }; }, [saveCurrentTextObject, initialTextValue, history, setInputEditor, settingsService]); diff --git a/src/plugins/console/public/application/containers/settings.tsx b/src/plugins/console/public/application/containers/settings.tsx index e34cfcac8096bb..81938a83435de9 100644 --- a/src/plugins/console/public/application/containers/settings.tsx +++ b/src/plugins/console/public/application/containers/settings.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { AutocompleteOptions, DevToolsSettingsModal } from '../components'; // @ts-ignore -import mappings from '../../lib/mappings/mappings'; +import { retrieveAutoCompleteInfo } from '../../lib/mappings/mappings'; import { useServicesContext, useEditorActionContext } from '../contexts'; import { DevToolsSettings, Settings as SettingsService } from '../../services'; @@ -33,7 +33,7 @@ const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToo }; const refreshAutocompleteSettings = (settings: SettingsService, selectedSettings: any) => { - mappings.retrieveAutoCompleteInfo(settings, selectedSettings); + retrieveAutoCompleteInfo(settings, selectedSettings); }; const fetchAutocompleteSettingsIfNeeded = ( @@ -61,10 +61,10 @@ const fetchAutocompleteSettingsIfNeeded = ( }, {} ); - mappings.retrieveAutoCompleteInfo(settings, changedSettings); + retrieveAutoCompleteInfo(settings, changedSettings); } else if (isPollingChanged && newSettings.polling) { // If the user has turned polling on, then we'll fetch all selected autocomplete settings. - mappings.retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); + retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); } } }; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index dde793d9b9691e..f0ce61f1d34016 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -24,7 +24,7 @@ import { sendRequestToES } from './send_request_to_es'; import { track } from './track'; // @ts-ignore -import mappings from '../../../lib/mappings/mappings'; +import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; export const useSendCurrentRequestToES = () => { const { @@ -73,7 +73,7 @@ export const useSendCurrentRequestToES = () => { // or templates may have changed, so we'll need to update this data. Assume that if // the user disables polling they're trying to optimize performance or otherwise // preserve resources, so they won't want this request sent either. - mappings.retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); + retrieveAutoCompleteInfo(settings, settings.getAutocomplete()); } dispatch({ diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js index 5c86b0a1d20924..1db9ca7bc0a865 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js @@ -17,7 +17,7 @@ * under the License. */ import '../legacy_core_editor.test.mocks'; -const $ = require('jquery'); +import $ from 'jquery'; import RowParser from '../../../../lib/row_parser'; import ace from 'brace'; import { createReadOnlyAceEditor } from '../create_readonly'; 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 d763db7ae5d795..77b4ba8cea6ffc 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 @@ -19,20 +19,21 @@ import ace from 'brace'; import { workerModule } from './worker'; +import { ScriptMode } from './script'; const oop = ace.acequire('ace/lib/oop'); const TextMode = ace.acequire('ace/mode/text').Mode; -const ScriptMode = require('./script').ScriptMode; + 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; const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; -const HighlightRules = require('./input_highlight_rules').InputHighlightRules; +import { InputHighlightRules } from './input_highlight_rules'; export function Mode() { - this.$tokenizer = new AceTokenizer(new HighlightRules().getRules()); + this.$tokenizer = new AceTokenizer(new InputHighlightRules().getRules()); this.$outdent = new MatchingBraceOutdent(); this.$behaviour = new CstyleBehaviour(); this.foldingRules = new CStyleFoldMode(); 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 29f192f4ea858e..1558cf0cb55544 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 @@ -17,7 +17,7 @@ * under the License. */ -const ace = require('brace'); +import ace from 'brace'; import { addXJsonToRules } from '../../../../../../es_ui_shared/public'; export function addEOL(tokens, reg, nextIfEOL, normalNext) { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js index 40e3128e396a37..5ad34532d18610 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js @@ -18,11 +18,11 @@ */ import ace from 'brace'; -require('./output_highlight_rules'); + +import { OutputJsonHighlightRules } from './output_highlight_rules'; const oop = ace.acequire('ace/lib/oop'); const JSONMode = ace.acequire('ace/mode/json').Mode; -const HighlightRules = require('./output_highlight_rules').OutputJsonHighlightRules; 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; @@ -30,7 +30,7 @@ ace.acequire('ace/worker/worker_client'); const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; export function Mode() { - this.$tokenizer = new AceTokenizer(new HighlightRules().getRules()); + this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules()); this.$outdent = new MatchingBraceOutdent(); this.$behaviour = new CstyleBehaviour(); this.foldingRules = new CStyleFoldMode(); 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 c9d538ab6ceb17..448fd847aeacda 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 @@ -17,7 +17,7 @@ * under the License. */ -const ace = require('brace'); +import ace from 'brace'; import 'brace/mode/json'; import { addXJsonToRules } from '../../../../../../es_ui_shared/public'; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js b/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js index f79a171c650826..8a0eb9a03480bf 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js @@ -18,7 +18,7 @@ */ /* eslint import/no-unresolved: 0 */ -const ace = require('brace'); +import ace from 'brace'; ace.define('ace/theme/sense-dark', ['require', 'exports', 'module'], function(require, exports) { exports.isDark = true; diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index c5a0c2ebddf718..edd09885c1ad2a 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -19,10 +19,10 @@ import '../sense_editor.test.mocks'; import { create } from '../create'; import _ from 'lodash'; -const $ = require('jquery'); +import $ from 'jquery'; -const kb = require('../../../../lib/kb/kb'); -const mappings = require('../../../../lib/mappings/mappings'); +import * as kb from '../../../../lib/kb/kb'; +import * as mappings from '../../../../lib/mappings/mappings'; describe('Integration', () => { let senseEditor; 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 34b4cad7fbb6bb..219e6262ab346c 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 @@ -23,7 +23,7 @@ import _ from 'lodash'; import { create } from '../create'; import { collapseLiteralStrings } from '../../../../../../es_ui_shared/public'; -const editorInput1 = require('./editor_input1.txt'); +import editorInput1 from './editor_input1.txt'; describe('Editor', () => { let input; diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js b/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js index 0758a756955662..ebde8c39cffbcb 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js +++ b/src/plugins/console/public/lib/autocomplete/__jest__/url_autocomplete.test.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { URL_PATH_END_MARKER, UrlPatternMatcher, diff --git a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js b/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js index 72fce53c4f1fef..286aefcd133a09 100644 --- a/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js +++ b/src/plugins/console/public/lib/autocomplete/__jest__/url_params.test.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { UrlParams } from '../../autocomplete/url_params'; import { populateContext } from '../../autocomplete/engine'; diff --git a/src/plugins/console/public/lib/autocomplete/body_completer.js b/src/plugins/console/public/lib/autocomplete/body_completer.js index 1aa315c50b9bf1..9bb1f14a6266ae 100644 --- a/src/plugins/console/public/lib/autocomplete/body_completer.js +++ b/src/plugins/console/public/lib/autocomplete/body_completer.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { WalkingState, walkTokenPath, wrapComponentWithDefaults } from './engine'; import { ConstantComponent, diff --git a/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js index 0574ffbcfc3a97..80f65406cf5d30 100644 --- a/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/field_autocomplete_component.js @@ -17,11 +17,11 @@ * under the License. */ import _ from 'lodash'; -import mappings from '../../mappings/mappings'; +import { getFields } from '../../mappings/mappings'; import { ListComponent } from './list_component'; function FieldGenerator(context) { - return _.map(mappings.getFields(context.indices, context.types), function(field) { + return _.map(getFields(context.indices, context.types), function(field) { return { name: field.name, meta: field.type }; }); } diff --git a/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js index 03d67c9e27ee87..ec6f24253e78d0 100644 --- a/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/index_autocomplete_component.js @@ -17,14 +17,14 @@ * under the License. */ import _ from 'lodash'; -import mappings from '../../mappings/mappings'; +import { getIndices } from '../../mappings/mappings'; import { ListComponent } from './list_component'; function nonValidIndexType(token) { return !(token === '_all' || token[0] !== '_'); } export class IndexAutocompleteComponent extends ListComponent { constructor(name, parent, multiValued) { - super(name, mappings.getIndices, parent, multiValued); + super(name, getIndices, parent, multiValued); } validateTokens(tokens) { if (!this.multiValued && tokens.length > 1) { diff --git a/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js index cc62a2f9eeea6c..14141980d493d7 100644 --- a/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/template_autocomplete_component.js @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import mappings from '../../mappings/mappings'; +import { getTemplates } from '../../mappings/mappings'; import { ListComponent } from './list_component'; export class TemplateAutocompleteComponent extends ListComponent { constructor(name, parent) { - super(name, mappings.getTemplates, parent, true, true); + super(name, getTemplates, parent, true, true); } getContextKey() { return 'template'; diff --git a/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js index 507817c1ed8c56..03d85eccaf3855 100644 --- a/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/type_autocomplete_component.js @@ -18,9 +18,9 @@ */ import _ from 'lodash'; import { ListComponent } from './list_component'; -import mappings from '../../mappings/mappings'; +import { getTypes } from '../../mappings/mappings'; function TypeGenerator(context) { - return mappings.getTypes(context.indices); + return getTypes(context.indices); } function nonValidIndexType(token) { return !(token === '_all' || token[0] !== '_'); diff --git a/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js index 26b7bd5c48c999..14b77d4e706255 100644 --- a/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js +++ b/src/plugins/console/public/lib/autocomplete/components/username_autocomplete_component.js @@ -17,14 +17,14 @@ * under the License. */ import _ from 'lodash'; -import mappings from '../../mappings/mappings'; +import { getIndices } from '../../mappings/mappings'; import { ListComponent } from './list_component'; function nonValidUsernameType(token) { return token[0] === '_'; } export class UsernameAutocompleteComponent extends ListComponent { constructor(name, parent, multiValued) { - super(name, mappings.getIndices, parent, multiValued); + super(name, getIndices, parent, multiValued); } validateTokens(tokens) { if (!this.multiValued && tokens.length > 1) { diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 7b64d91c953742..ae943a74ffb3a4 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; export function wrapComponentWithDefaults(component, defaults) { const originalGetTerms = component.getTerms; diff --git a/src/plugins/console/public/lib/autocomplete/url_params.js b/src/plugins/console/public/lib/autocomplete/url_params.js index 3f05b9ce85aab9..0519a2daade878 100644 --- a/src/plugins/console/public/lib/autocomplete/url_params.js +++ b/src/plugins/console/public/lib/autocomplete/url_params.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { ConstantComponent, ListComponent, SharedComponent } from './components'; export class ParamComponent extends ConstantComponent { diff --git a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js b/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js index 49a54eaefa9efc..6a8caebfc68746 100644 --- a/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js +++ b/src/plugins/console/public/lib/curl_parsing/__tests__/curl_parsing.test.js @@ -17,15 +17,15 @@ * under the License. */ -const _ = require('lodash'); -const curl = require('../curl'); +import _ from 'lodash'; +import { detectCURL, parseCURL } from '../curl'; import curlTests from './curl_parsing.txt'; describe('CURL', () => { const notCURLS = ['sldhfsljfhs', 's;kdjfsldkfj curl -XDELETE ""', '{ "hello": 1 }']; _.each(notCURLS, function(notCURL, i) { test('cURL Detection - broken strings ' + i, function() { - expect(curl.detectCURL(notCURL)).toEqual(false); + expect(detectCURL(notCURL)).toEqual(false); }); }); @@ -39,8 +39,8 @@ describe('CURL', () => { const response = fixture[2].trim(); test('cURL Detection - ' + name, function() { - expect(curl.detectCURL(curlText)).toBe(true); - const r = curl.parseCURL(curlText); + expect(detectCURL(curlText)).toBe(true); + const r = parseCURL(curlText); expect(r).toEqual(response); }); }); diff --git a/src/plugins/console/public/lib/kb/__tests__/kb.test.js b/src/plugins/console/public/lib/kb/__tests__/kb.test.js index c2c69314a172de..c80f5671449b36 100644 --- a/src/plugins/console/public/lib/kb/__tests__/kb.test.js +++ b/src/plugins/console/public/lib/kb/__tests__/kb.test.js @@ -21,8 +21,8 @@ import _ from 'lodash'; import { populateContext } from '../../autocomplete/engine'; import '../../../application/models/sense_editor/sense_editor.test.mocks'; -const kb = require('../../kb'); -const mappings = require('../../mappings/mappings'); +import * as kb from '../../kb'; +import * as mappings from '../../mappings/mappings'; describe('Knowledge base', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/kb/api.js b/src/plugins/console/public/lib/kb/api.js index eeec87060b7704..c418a7cb414efc 100644 --- a/src/plugins/console/public/lib/kb/api.js +++ b/src/plugins/console/public/lib/kb/api.js @@ -17,7 +17,7 @@ * under the License. */ -const _ = require('lodash'); +import _ from 'lodash'; import { UrlPatternMatcher } from '../autocomplete/components'; import { UrlParams } from '../autocomplete/url_params'; import { diff --git a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js b/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js index 27b3ce26b55883..292b1b4fb1bf58 100644 --- a/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js +++ b/src/plugins/console/public/lib/mappings/__tests__/mapping.test.js @@ -17,7 +17,7 @@ * under the License. */ import '../../../application/models/sense_editor/sense_editor.test.mocks'; -const mappings = require('../mappings'); +import * as mappings from '../mappings'; describe('Mappings', () => { beforeEach(() => { diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index 330147118d42cc..b5bcc2b105996b 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -17,9 +17,9 @@ * under the License. */ -const $ = require('jquery'); -const _ = require('lodash'); -const es = require('../es/es'); +import $ from 'jquery'; +import _ from 'lodash'; +import * as es from '../es/es'; // NOTE: If this value ever changes to be a few seconds or less, it might introduce flakiness // due to timing issues in our app.js tests. @@ -32,7 +32,7 @@ let templates = []; const mappingObj = {}; -function expandAliases(indicesOrAliases) { +export function expandAliases(indicesOrAliases) { // takes a list of indices or aliases or a string which may be either and returns a list of indices // returns a list for multiple values or a string for a single. @@ -60,11 +60,11 @@ function expandAliases(indicesOrAliases) { return ret.length > 1 ? ret : ret[0]; } -function getTemplates() { +export function getTemplates() { return [...templates]; } -function getFields(indices, types) { +export function getFields(indices, types) { // get fields for indices and types. Both can be a list, a string or null (meaning all). let ret = []; indices = expandAliases(indices); @@ -103,7 +103,7 @@ function getFields(indices, types) { }); } -function getTypes(indices) { +export function getTypes(indices) { let ret = []; indices = expandAliases(indices); if (typeof indices === 'string') { @@ -129,7 +129,7 @@ function getTypes(indices) { return _.uniq(ret); } -function getIndices(includeAliases) { +export function getIndices(includeAliases) { const ret = []; $.each(perIndexTypes, function(index) { ret.push(index); @@ -200,7 +200,7 @@ function loadTemplates(templatesObject = {}) { templates = Object.keys(templatesObject); } -function loadMappings(mappings) { +export function loadMappings(mappings) { perIndexTypes = {}; $.each(mappings, function(index, indexMapping) { @@ -224,7 +224,7 @@ function loadMappings(mappings) { }); } -function loadAliases(aliases) { +export function loadAliases(aliases) { perAliasIndexes = {}; $.each(aliases || {}, function(index, omdexAliases) { // verify we have an index defined. useful when mapping loading is disabled @@ -246,7 +246,7 @@ function loadAliases(aliases) { perAliasIndexes._all = getIndices(false); } -function clear() { +export function clear() { perIndexTypes = {}; perAliasIndexes = {}; templates = []; @@ -285,7 +285,7 @@ function retrieveSettings(settingsKey, settingsToRetrieve) { // unchanged alone (both selected and unselected). // 3. Poll: Use saved. Fetch selected. Ignore unselected. -function clearSubscriptions() { +export function clearSubscriptions() { if (pollTimeoutId) { clearTimeout(pollTimeoutId); } @@ -296,7 +296,7 @@ function clearSubscriptions() { * @param settings Settings A way to retrieve the current settings * @param settingsToRetrieve any */ -function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { +export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { clearSubscriptions(); const mappingPromise = retrieveSettings('fields', settingsToRetrieve); @@ -341,16 +341,3 @@ function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { }, POLL_INTERVAL); }); } - -export default { - getFields, - getTemplates, - getIndices, - getTypes, - loadMappings, - loadAliases, - expandAliases, - clear, - retrieveAutoCompleteInfo, - clearSubscriptions, -}; 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 6115be3c84ed93..3a2e6a54c1328f 100644 --- a/src/plugins/console/public/lib/utils/__tests__/utils.test.js +++ b/src/plugins/console/public/lib/utils/__tests__/utils.test.js @@ -17,7 +17,7 @@ * under the License. */ -const utils = require('../'); +import * as utils from '../'; describe('Utils class', () => { test('extract deprecation messages', function() { diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js b/x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js index 02165b659407fc..31dbddfcd17ea7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js +++ b/x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js @@ -6,15 +6,12 @@ /* eslint-env jest */ -function $() { +export function $() { return { on: jest.fn(), off: jest.fn(), + plot: () => ({ + shutdown: jest.fn(), + }), }; } - -$.plot = () => ({ - shutdown: jest.fn(), -}); - -module.exports = $; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts index ff051d470531bc..ce40a0e46b0a06 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import Chance from 'chance'; import { getRandomString } from '../../../../../../test_utils'; import { FollowerIndex } from '../../../../common/types'; -const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires const chance = new Chance(); interface FollowerIndexMock { diff --git a/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js index 90538ae652c082..872df0cddca3c9 100644 --- a/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js +++ b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js @@ -5,7 +5,7 @@ */ import { cleanGeometry, geoJsonCleanAndValidate } from './geo_json_clean_and_validate'; -const jsts = require('jsts'); +import * as jsts from 'jsts'; describe('geo_json_clean_and_validate', () => { const reader = new jsts.io.GeoJSONReader(); diff --git a/x-pack/plugins/file_upload/public/util/pattern_reader.js b/x-pack/plugins/file_upload/public/util/pattern_reader.js index 78f1c4f2175197..152e0f7e545807 100644 --- a/x-pack/plugins/file_upload/public/util/pattern_reader.js +++ b/x-pack/plugins/file_upload/public/util/pattern_reader.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -const oboe = require('oboe'); +import oboe from 'oboe'; export class PatternReader { constructor({ onFeatureDetect, onStreamComplete }) { diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.js b/x-pack/plugins/graph/public/angular/graph_client_workspace.js index a7d98a42404ecc..bcd31716b6d64e 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.js +++ b/x-pack/plugins/graph/public/angular/graph_client_workspace.js @@ -7,1576 +7,1569 @@ import $ from 'jquery'; // Kibana wrapper -const d3 = require('d3'); - -module.exports = (function() { - // Pluggable function to handle the comms with a server. Default impl here is - // for use outside of Kibana server with direct access to elasticsearch - let graphExplorer = function(indexName, typeName, request, responseHandler) { - const dataForServer = JSON.stringify(request); - $.ajax({ - type: 'POST', - url: 'http://localhost:9200/' + indexName + '/_graph/explore', - dataType: 'json', - contentType: 'application/json;charset=utf-8', - async: true, - data: dataForServer, - success: function(data) { - responseHandler(data); - }, - }); +import d3 from 'd3'; + +// Pluggable function to handle the comms with a server. Default impl here is +// for use outside of Kibana server with direct access to elasticsearch +let graphExplorer = function(indexName, typeName, request, responseHandler) { + const dataForServer = JSON.stringify(request); + $.ajax({ + type: 'POST', + url: 'http://localhost:9200/' + indexName + '/_graph/explore', + dataType: 'json', + contentType: 'application/json;charset=utf-8', + async: true, + data: dataForServer, + success: function(data) { + responseHandler(data); + }, + }); +}; +let searcher = function(indexName, request, responseHandler) { + const dataForServer = JSON.stringify(request); + $.ajax({ + type: 'POST', + url: 'http://localhost:9200/' + indexName + '/_search?rest_total_hits_as_int=true', + dataType: 'json', + contentType: 'application/json;charset=utf-8', //Not sure why this was necessary - worked without elsewhere + async: true, + data: dataForServer, + success: function(data) { + responseHandler(data); + }, + }); +}; + +// ====== Undo operations ============= + +function AddNodeOperation(node, owner) { + const self = this; + const vm = owner; + self.node = node; + self.undo = function() { + vm.arrRemove(vm.nodes, self.node); + vm.arrRemove(vm.selectedNodes, self.node); + self.node.isSelected = false; + + delete vm.nodesMap[self.node.id]; }; - let searcher = function(indexName, request, responseHandler) { - const dataForServer = JSON.stringify(request); - $.ajax({ - type: 'POST', - url: 'http://localhost:9200/' + indexName + '/_search?rest_total_hits_as_int=true', - dataType: 'json', - contentType: 'application/json;charset=utf-8', //Not sure why this was necessary - worked without elsewhere - async: true, - data: dataForServer, - success: function(data) { - responseHandler(data); - }, - }); + self.redo = function() { + vm.nodes.push(self.node); + vm.nodesMap[self.node.id] = self.node; }; - - // ====== Undo operations ============= - - function AddNodeOperation(node, owner) { - const self = this; - const vm = owner; - self.node = node; - self.undo = function() { - vm.arrRemove(vm.nodes, self.node); - vm.arrRemove(vm.selectedNodes, self.node); - self.node.isSelected = false; - - delete vm.nodesMap[self.node.id]; - }; - self.redo = function() { - vm.nodes.push(self.node); - vm.nodesMap[self.node.id] = self.node; - }; - } - - function AddEdgeOperation(edge, owner) { - const self = this; - const vm = owner; - self.edge = edge; - self.undo = function() { - vm.arrRemove(vm.edges, self.edge); - delete vm.edgesMap[self.edge.id]; - }; - self.redo = function() { - vm.edges.push(self.edge); - vm.edgesMap[self.edge.id] = self.edge; - }; - } - - function ReverseOperation(operation) { - const self = this; - const reverseOperation = operation; - self.undo = reverseOperation.redo; - self.redo = reverseOperation.undo; - } - - function GroupOperation(receiver, orphan) { - const self = this; - self.receiver = receiver; - self.orphan = orphan; - self.undo = function() { - self.orphan.parent = undefined; - }; - self.redo = function() { - self.orphan.parent = self.receiver; - }; +} + +function AddEdgeOperation(edge, owner) { + const self = this; + const vm = owner; + self.edge = edge; + self.undo = function() { + vm.arrRemove(vm.edges, self.edge); + delete vm.edgesMap[self.edge.id]; + }; + self.redo = function() { + vm.edges.push(self.edge); + vm.edgesMap[self.edge.id] = self.edge; + }; +} + +function ReverseOperation(operation) { + const self = this; + const reverseOperation = operation; + self.undo = reverseOperation.redo; + self.redo = reverseOperation.undo; +} + +function GroupOperation(receiver, orphan) { + const self = this; + self.receiver = receiver; + self.orphan = orphan; + self.undo = function() { + self.orphan.parent = undefined; + }; + self.redo = function() { + self.orphan.parent = self.receiver; + }; +} + +function UnGroupOperation(parent, child) { + const self = this; + self.parent = parent; + self.child = child; + self.undo = function() { + self.child.parent = self.parent; + }; + self.redo = function() { + self.child.parent = undefined; + }; +} + +// The main constructor for our GraphWorkspace +function GraphWorkspace(options) { + const self = this; + this.blacklistedNodes = []; + this.options = options; + this.undoLog = []; + this.redoLog = []; + this.selectedNodes = []; + + if (!options) { + this.options = {}; } - - function UnGroupOperation(parent, child) { - const self = this; - self.parent = parent; - self.child = child; - self.undo = function() { - self.child.parent = self.parent; - }; - self.redo = function() { - self.child.parent = undefined; - }; + this.nodesMap = {}; + this.edgesMap = {}; + this.searchTerm = ''; + + //A sequence number used to know when a node was added + this.seqNumber = 0; + + this.nodes = []; + this.edges = []; + this.lastRequest = null; + this.lastResponse = null; + this.changeHandler = options.changeHandler; + if (options.graphExploreProxy) { + graphExplorer = options.graphExploreProxy; } - - function createWorkspace(options) { - return new GraphWorkspace(options); + if (options.searchProxy) { + searcher = options.searchProxy; } - // The main constructor for our GraphWorkspace - function GraphWorkspace(options) { - const self = this; - this.blacklistedNodes = []; - this.options = options; - this.undoLog = []; - this.redoLog = []; - this.selectedNodes = []; - - if (!options) { - this.options = {}; + this.addUndoLogEntry = function(undoOperations) { + self.undoLog.push(undoOperations); + if (self.undoLog.length > 50) { + //Remove the oldest + self.undoLog.splice(0, 1); } - this.nodesMap = {}; - this.edgesMap = {}; - this.searchTerm = ''; - - //A sequence number used to know when a node was added - this.seqNumber = 0; + self.redoLog = []; + }; - this.nodes = []; - this.edges = []; - this.lastRequest = null; - this.lastResponse = null; - this.changeHandler = options.changeHandler; - if (options.graphExploreProxy) { - graphExplorer = options.graphExploreProxy; + this.undo = function() { + const lastOps = this.undoLog.pop(); + if (lastOps) { + this.stopLayout(); + this.redoLog.push(lastOps); + lastOps.forEach(ops => ops.undo()); + this.runLayout(); } - if (options.searchProxy) { - searcher = options.searchProxy; + }; + this.redo = function() { + const lastOps = this.redoLog.pop(); + if (lastOps) { + this.stopLayout(); + this.undoLog.push(lastOps); + lastOps.forEach(ops => ops.redo()); + this.runLayout(); } + }; - this.addUndoLogEntry = function(undoOperations) { - self.undoLog.push(undoOperations); - if (self.undoLog.length > 50) { - //Remove the oldest - self.undoLog.splice(0, 1); + //Determines if 2 nodes are connected via an edge + this.areLinked = function(a, b) { + if (a === b) return true; + this.edges.forEach(e => { + if (e.source === a && e.target === b) { + return true; } - self.redoLog = []; - }; - - this.undo = function() { - const lastOps = this.undoLog.pop(); - if (lastOps) { - this.stopLayout(); - this.redoLog.push(lastOps); - lastOps.forEach(ops => ops.undo()); - this.runLayout(); + if (e.source === b && e.target === a) { + return true; } - }; - this.redo = function() { - const lastOps = this.redoLog.pop(); - if (lastOps) { - this.stopLayout(); - this.undoLog.push(lastOps); - lastOps.forEach(ops => ops.redo()); - this.runLayout(); - } - }; - - //Determines if 2 nodes are connected via an edge - this.areLinked = function(a, b) { - if (a === b) return true; - this.edges.forEach(e => { - if (e.source === a && e.target === b) { - return true; - } - if (e.source === b && e.target === a) { - return true; - } - }); - return false; - }; + }); + return false; + }; - //======== Selection functions ======== + //======== Selection functions ======== - this.selectAll = function() { - self.selectedNodes = []; - self.nodes.forEach(node => { - if (node.parent === undefined) { - node.isSelected = true; - self.selectedNodes.push(node); - } else { - node.isSelected = false; - } - }); - }; - - this.selectNone = function() { - self.selectedNodes = []; - self.nodes.forEach(node => { + this.selectAll = function() { + self.selectedNodes = []; + self.nodes.forEach(node => { + if (node.parent === undefined) { + node.isSelected = true; + self.selectedNodes.push(node); + } else { node.isSelected = false; - }); - }; + } + }); + }; - this.selectInvert = function() { - self.selectedNodes = []; - self.nodes.forEach(node => { - if (node.parent !== undefined) { - return; - } - node.isSelected = !node.isSelected; - if (node.isSelected) { - self.selectedNodes.push(node); - } - }); - }; + this.selectNone = function() { + self.selectedNodes = []; + self.nodes.forEach(node => { + node.isSelected = false; + }); + }; - this.selectNodes = function(nodes) { - nodes.forEach(node => { - node.isSelected = true; - if (self.selectedNodes.indexOf(node) < 0) { - self.selectedNodes.push(node); - } - }); - }; + this.selectInvert = function() { + self.selectedNodes = []; + self.nodes.forEach(node => { + if (node.parent !== undefined) { + return; + } + node.isSelected = !node.isSelected; + if (node.isSelected) { + self.selectedNodes.push(node); + } + }); + }; - this.selectNode = function(node) { + this.selectNodes = function(nodes) { + nodes.forEach(node => { node.isSelected = true; if (self.selectedNodes.indexOf(node) < 0) { self.selectedNodes.push(node); } - }; + }); + }; - this.deleteSelection = function() { - let allAndGrouped = self.returnUnpackedGroupeds(self.selectedNodes); + this.selectNode = function(node) { + node.isSelected = true; + if (self.selectedNodes.indexOf(node) < 0) { + self.selectedNodes.push(node); + } + }; - // Nothing selected so process all nodes - if (allAndGrouped.length === 0) { - allAndGrouped = self.nodes.slice(0); - } + this.deleteSelection = function() { + let allAndGrouped = self.returnUnpackedGroupeds(self.selectedNodes); - const undoOperations = []; - allAndGrouped.forEach(node => { - //We set selected to false because despite being deleted, node objects sit in an undo log - node.isSelected = false; - delete self.nodesMap[node.id]; - undoOperations.push(new ReverseOperation(new AddNodeOperation(node, self))); - }); - self.arrRemoveAll(self.nodes, allAndGrouped); - self.arrRemoveAll(self.selectedNodes, allAndGrouped); + // Nothing selected so process all nodes + if (allAndGrouped.length === 0) { + allAndGrouped = self.nodes.slice(0); + } - const danglingEdges = self.edges.filter(function(edge) { - return self.nodes.indexOf(edge.source) < 0 || self.nodes.indexOf(edge.target) < 0; - }); - danglingEdges.forEach(edge => { - delete self.edgesMap[edge.id]; - undoOperations.push(new ReverseOperation(new AddEdgeOperation(edge, self))); - }); - self.addUndoLogEntry(undoOperations); - self.arrRemoveAll(self.edges, danglingEdges); - self.runLayout(); - }; + const undoOperations = []; + allAndGrouped.forEach(node => { + //We set selected to false because despite being deleted, node objects sit in an undo log + node.isSelected = false; + delete self.nodesMap[node.id]; + undoOperations.push(new ReverseOperation(new AddNodeOperation(node, self))); + }); + self.arrRemoveAll(self.nodes, allAndGrouped); + self.arrRemoveAll(self.selectedNodes, allAndGrouped); - this.selectNeighbours = function() { - const newSelections = []; - self.edges.forEach(edge => { - if (!edge.topSrc.isSelected) { - if (self.selectedNodes.indexOf(edge.topTarget) >= 0) { - if (newSelections.indexOf(edge.topSrc) < 0) { - newSelections.push(edge.topSrc); - } + const danglingEdges = self.edges.filter(function(edge) { + return self.nodes.indexOf(edge.source) < 0 || self.nodes.indexOf(edge.target) < 0; + }); + danglingEdges.forEach(edge => { + delete self.edgesMap[edge.id]; + undoOperations.push(new ReverseOperation(new AddEdgeOperation(edge, self))); + }); + self.addUndoLogEntry(undoOperations); + self.arrRemoveAll(self.edges, danglingEdges); + self.runLayout(); + }; + + this.selectNeighbours = function() { + const newSelections = []; + self.edges.forEach(edge => { + if (!edge.topSrc.isSelected) { + if (self.selectedNodes.indexOf(edge.topTarget) >= 0) { + if (newSelections.indexOf(edge.topSrc) < 0) { + newSelections.push(edge.topSrc); } } - if (!edge.topTarget.isSelected) { - if (self.selectedNodes.indexOf(edge.topSrc) >= 0) { - if (newSelections.indexOf(edge.topTarget) < 0) { - newSelections.push(edge.topTarget); - } + } + if (!edge.topTarget.isSelected) { + if (self.selectedNodes.indexOf(edge.topSrc) >= 0) { + if (newSelections.indexOf(edge.topTarget) < 0) { + newSelections.push(edge.topTarget); } } - }); - newSelections.forEach(newlySelectedNode => { - self.selectedNodes.push(newlySelectedNode); - newlySelectedNode.isSelected = true; - }); - }; - - this.selectNone = function() { - self.selectedNodes.forEach(node => { - node.isSelected = false; - }); - self.selectedNodes = []; - }; + } + }); + newSelections.forEach(newlySelectedNode => { + self.selectedNodes.push(newlySelectedNode); + newlySelectedNode.isSelected = true; + }); + }; - this.deselectNode = function(node) { + this.selectNone = function() { + self.selectedNodes.forEach(node => { node.isSelected = false; - self.arrRemove(self.selectedNodes, node); - }; - - this.getAllSelectedNodes = function() { - return this.returnUnpackedGroupeds(self.selectedNodes); - }; + }); + self.selectedNodes = []; + }; - this.colorSelected = function(colorNum) { - self.getAllSelectedNodes().forEach(node => { - node.color = colorNum; - }); - }; + this.deselectNode = function(node) { + node.isSelected = false; + self.arrRemove(self.selectedNodes, node); + }; - this.getSelectionsThatAreGrouped = function() { - const result = []; - self.selectedNodes.forEach(node => { - if (node.numChildren > 0) { - result.push(node); - } - }); - return result; - }; + this.getAllSelectedNodes = function() { + return this.returnUnpackedGroupeds(self.selectedNodes); + }; - this.ungroupSelection = function() { - self.getSelectionsThatAreGrouped().forEach(node => { - self.ungroup(node); - }); - }; + this.colorSelected = function(colorNum) { + self.getAllSelectedNodes().forEach(node => { + node.color = colorNum; + }); + }; - this.toggleNodeSelection = function(node) { - if (node.isSelected) { - self.deselectNode(node); - } else { - node.isSelected = true; - self.selectedNodes.push(node); + this.getSelectionsThatAreGrouped = function() { + const result = []; + self.selectedNodes.forEach(node => { + if (node.numChildren > 0) { + result.push(node); } - return node.isSelected; - }; + }); + return result; + }; - this.returnUnpackedGroupeds = function(topLevelNodeArray) { - //Gather any grouped nodes that are part of this top-level selection - const result = topLevelNodeArray.slice(); + this.ungroupSelection = function() { + self.getSelectionsThatAreGrouped().forEach(node => { + self.ungroup(node); + }); + }; - // We iterate over edges not nodes because edges conveniently hold the top-most - // node information. + this.toggleNodeSelection = function(node) { + if (node.isSelected) { + self.deselectNode(node); + } else { + node.isSelected = true; + self.selectedNodes.push(node); + } + return node.isSelected; + }; - const edges = this.edges; - for (let i = 0; i < edges.length; i++) { - const edge = edges[i]; + this.returnUnpackedGroupeds = function(topLevelNodeArray) { + //Gather any grouped nodes that are part of this top-level selection + const result = topLevelNodeArray.slice(); - const topLevelSource = edge.topSrc; - const topLevelTarget = edge.topTarget; + // We iterate over edges not nodes because edges conveniently hold the top-most + // node information. - if (result.indexOf(topLevelTarget) >= 0) { - //visible top-level node is selected - add all nesteds starting from bottom up - let target = edge.target; - while (target.parent !== undefined) { - if (result.indexOf(target) < 0) { - result.push(target); - } - target = target.parent; + const edges = this.edges; + for (let i = 0; i < edges.length; i++) { + const edge = edges[i]; + + const topLevelSource = edge.topSrc; + const topLevelTarget = edge.topTarget; + + if (result.indexOf(topLevelTarget) >= 0) { + //visible top-level node is selected - add all nesteds starting from bottom up + let target = edge.target; + while (target.parent !== undefined) { + if (result.indexOf(target) < 0) { + result.push(target); } + target = target.parent; } + } - if (result.indexOf(topLevelSource) >= 0) { - //visible top-level node is selected - add all nesteds starting from bottom up - let source = edge.source; - while (source.parent !== undefined) { - if (result.indexOf(source) < 0) { - result.push(source); - } - source = source.parent; + if (result.indexOf(topLevelSource) >= 0) { + //visible top-level node is selected - add all nesteds starting from bottom up + let source = edge.source; + while (source.parent !== undefined) { + if (result.indexOf(source) < 0) { + result.push(source); } + source = source.parent; } - } //end of edges loop + } + } //end of edges loop - return result; - }; + return result; + }; - // ======= Miscellaneous functions + // ======= Miscellaneous functions - this.clearGraph = function() { - this.stopLayout(); - this.nodes = []; - this.edges = []; - this.undoLog = []; - this.redoLog = []; - this.nodesMap = {}; - this.edgesMap = {}; - this.blacklistedNodes = []; - this.selectedNodes = []; - this.lastResponse = null; - }; + this.clearGraph = function() { + this.stopLayout(); + this.nodes = []; + this.edges = []; + this.undoLog = []; + this.redoLog = []; + this.nodesMap = {}; + this.edgesMap = {}; + this.blacklistedNodes = []; + this.selectedNodes = []; + this.lastResponse = null; + }; - this.arrRemoveAll = function remove(arr, items) { - for (let i = items.length; i--; ) { - self.arrRemove(arr, items[i]); - } - }; + this.arrRemoveAll = function remove(arr, items) { + for (let i = items.length; i--; ) { + self.arrRemove(arr, items[i]); + } + }; - this.arrRemove = function remove(arr, item) { - for (let i = arr.length; i--; ) { - if (arr[i] === item) { - arr.splice(i, 1); - } + this.arrRemove = function remove(arr, item) { + for (let i = arr.length; i--; ) { + if (arr[i] === item) { + arr.splice(i, 1); } - }; + } + }; - this.getNeighbours = function(node) { - const neighbourNodes = []; - self.edges.forEach(edge => { - if (edge.topSrc === edge.topTarget) { - return; - } - if (edge.topSrc === node) { - if (neighbourNodes.indexOf(edge.topTarget) < 0) { - neighbourNodes.push(edge.topTarget); - } - } - if (edge.topTarget === node) { - if (neighbourNodes.indexOf(edge.topSrc) < 0) { - neighbourNodes.push(edge.topSrc); - } + this.getNeighbours = function(node) { + const neighbourNodes = []; + self.edges.forEach(edge => { + if (edge.topSrc === edge.topTarget) { + return; + } + if (edge.topSrc === node) { + if (neighbourNodes.indexOf(edge.topTarget) < 0) { + neighbourNodes.push(edge.topTarget); } - }); - return neighbourNodes; - }; - - //Creates a query that represents a node - either simple term query or boolean if grouped - this.buildNodeQuery = function(topLevelNode) { - let containedNodes = [topLevelNode]; - containedNodes = self.returnUnpackedGroupeds(containedNodes); - if (containedNodes.length === 1) { - //Simple case - return a single-term query - const tq = {}; - tq[topLevelNode.data.field] = topLevelNode.data.term; - return { - term: tq, - }; } - const termsByField = {}; - containedNodes.forEach(node => { - let termsList = termsByField[node.data.field]; - if (!termsList) { - termsList = []; - termsByField[node.data.field] = termsList; + if (edge.topTarget === node) { + if (neighbourNodes.indexOf(edge.topSrc) < 0) { + neighbourNodes.push(edge.topSrc); } - termsList.push(node.data.term); - }); - //Single field case - if (Object.keys(termsByField).length === 1) { - return { - terms: termsByField, - }; } - //Multi-field case - build a bool query with per-field terms clauses. - const q = { - bool: { - should: [], - }, + }); + return neighbourNodes; + }; + + //Creates a query that represents a node - either simple term query or boolean if grouped + this.buildNodeQuery = function(topLevelNode) { + let containedNodes = [topLevelNode]; + containedNodes = self.returnUnpackedGroupeds(containedNodes); + if (containedNodes.length === 1) { + //Simple case - return a single-term query + const tq = {}; + tq[topLevelNode.data.field] = topLevelNode.data.term; + return { + term: tq, }; - for (const field in termsByField) { - if (termsByField.hasOwnProperty(field)) { - const tq = {}; - tq[field] = termsByField[field]; - q.bool.should.push({ - terms: tq, - }); - } + } + const termsByField = {}; + containedNodes.forEach(node => { + let termsList = termsByField[node.data.field]; + if (!termsList) { + termsList = []; + termsByField[node.data.field] = termsList; } - return q; + termsList.push(node.data.term); + }); + //Single field case + if (Object.keys(termsByField).length === 1) { + return { + terms: termsByField, + }; + } + //Multi-field case - build a bool query with per-field terms clauses. + const q = { + bool: { + should: [], + }, }; + for (const field in termsByField) { + if (termsByField.hasOwnProperty(field)) { + const tq = {}; + tq[field] = termsByField[field]; + q.bool.should.push({ + terms: tq, + }); + } + } + return q; + }; - //====== Layout functions ======== + //====== Layout functions ======== - this.stopLayout = function() { - if (this.force) { - this.force.stop(); - } - this.force = null; - }; + this.stopLayout = function() { + if (this.force) { + this.force.stop(); + } + this.force = null; + }; - this.runLayout = function() { - this.stopLayout(); - // The set of nodes and edges we present to the d3 layout algorithms - // is potentially a reduced set of nodes if the client has used any - // grouping of nodes into parent nodes. - const effectiveEdges = []; - self.edges.forEach(edge => { - let topSrc = edge.source; - let topTarget = edge.target; - while (topSrc.parent !== undefined) { - topSrc = topSrc.parent; - } - while (topTarget.parent !== undefined) { - topTarget = topTarget.parent; - } - edge.topSrc = topSrc; - edge.topTarget = topTarget; + this.runLayout = function() { + this.stopLayout(); + // The set of nodes and edges we present to the d3 layout algorithms + // is potentially a reduced set of nodes if the client has used any + // grouping of nodes into parent nodes. + const effectiveEdges = []; + self.edges.forEach(edge => { + let topSrc = edge.source; + let topTarget = edge.target; + while (topSrc.parent !== undefined) { + topSrc = topSrc.parent; + } + while (topTarget.parent !== undefined) { + topTarget = topTarget.parent; + } + edge.topSrc = topSrc; + edge.topTarget = topTarget; - if (topSrc !== topTarget) { - effectiveEdges.push({ - source: topSrc, - target: topTarget, - }); - } - }); - const visibleNodes = self.nodes.filter(function(n) { - return n.parent === undefined; - }); - //reset then roll-up all the counts - const allNodes = self.nodes; - allNodes.forEach(node => { - node.numChildren = 0; - }); + if (topSrc !== topTarget) { + effectiveEdges.push({ + source: topSrc, + target: topTarget, + }); + } + }); + const visibleNodes = self.nodes.filter(function(n) { + return n.parent === undefined; + }); + //reset then roll-up all the counts + const allNodes = self.nodes; + allNodes.forEach(node => { + node.numChildren = 0; + }); - for (const n in allNodes) { - if (!allNodes.hasOwnProperty(n)) { - continue; - } - let node = allNodes[n]; - while (node.parent !== undefined) { - node = node.parent; - node.numChildren = node.numChildren + 1; - } + for (const n in allNodes) { + if (!allNodes.hasOwnProperty(n)) { + continue; + } + let node = allNodes[n]; + while (node.parent !== undefined) { + node = node.parent; + node.numChildren = node.numChildren + 1; } - this.force = d3.layout - .force() - .nodes(visibleNodes) - .links(effectiveEdges) - .friction(0.8) - .linkDistance(100) - .charge(-1500) - .gravity(0.15) - .theta(0.99) - .alpha(0.5) - .size([800, 600]) - .on('tick', function() { - const nodeArray = self.nodes; - let hasRollups = false; - //Update the position of all "top level nodes" + } + this.force = d3.layout + .force() + .nodes(visibleNodes) + .links(effectiveEdges) + .friction(0.8) + .linkDistance(100) + .charge(-1500) + .gravity(0.15) + .theta(0.99) + .alpha(0.5) + .size([800, 600]) + .on('tick', function() { + const nodeArray = self.nodes; + let hasRollups = false; + //Update the position of all "top level nodes" + nodeArray.forEach(n => { + //Code to support roll-ups + if (n.parent === undefined) { + n.kx = n.x; + n.ky = n.y; + } else { + hasRollups = true; + } + }); + if (hasRollups) { nodeArray.forEach(n => { //Code to support roll-ups - if (n.parent === undefined) { - n.kx = n.x; - n.ky = n.y; - } else { - hasRollups = true; - } - }); - if (hasRollups) { - nodeArray.forEach(n => { - //Code to support roll-ups - if (n.parent !== undefined) { - // Is a grouped node - inherit parent's position so edges point into parent - // d3 thinks it has moved it to x and y but we have final say using kx and ky. - let topLevelNode = n.parent; - while (topLevelNode.parent !== undefined) { - topLevelNode = topLevelNode.parent; - } - - n.kx = topLevelNode.x; - n.ky = topLevelNode.y; + if (n.parent !== undefined) { + // Is a grouped node - inherit parent's position so edges point into parent + // d3 thinks it has moved it to x and y but we have final say using kx and ky. + let topLevelNode = n.parent; + while (topLevelNode.parent !== undefined) { + topLevelNode = topLevelNode.parent; } - }); - } - if (self.changeHandler) { - // Hook to allow any client to respond to position changes - // e.g. angular adjusts and repaints node positions on screen. - self.changeHandler(); - } - }); - this.force.start(); - }; - //========Grouping functions========== - - //Merges all selected nodes into node - this.groupSelections = function(node) { - const ops = []; - self.nodes.forEach(function(otherNode) { - if (otherNode !== node && otherNode.isSelected && otherNode.parent === undefined) { - otherNode.parent = node; - otherNode.isSelected = false; - self.arrRemove(self.selectedNodes, otherNode); - ops.push(new GroupOperation(node, otherNode)); + n.kx = topLevelNode.x; + n.ky = topLevelNode.y; + } + }); } - }); - self.selectNone(); - self.selectNode(node); - self.addUndoLogEntry(ops); - self.runLayout(); - }; - - this.mergeNeighbours = function(node) { - const neighbours = self.getNeighbours(node); - const ops = []; - neighbours.forEach(function(otherNode) { - if (otherNode !== node && otherNode.parent === undefined) { - otherNode.parent = node; - otherNode.isSelected = false; - self.arrRemove(self.selectedNodes, otherNode); - ops.push(new GroupOperation(node, otherNode)); + if (self.changeHandler) { + // Hook to allow any client to respond to position changes + // e.g. angular adjusts and repaints node positions on screen. + self.changeHandler(); } }); - self.addUndoLogEntry(ops); - self.runLayout(); - }; + this.force.start(); + }; - this.mergeSelections = function(targetNode) { - if (!targetNode) { - console.log('Error - merge called on undefined target'); - return; + //========Grouping functions========== + + //Merges all selected nodes into node + this.groupSelections = function(node) { + const ops = []; + self.nodes.forEach(function(otherNode) { + if (otherNode !== node && otherNode.isSelected && otherNode.parent === undefined) { + otherNode.parent = node; + otherNode.isSelected = false; + self.arrRemove(self.selectedNodes, otherNode); + ops.push(new GroupOperation(node, otherNode)); } - const selClone = self.selectedNodes.slice(); - const ops = []; - selClone.forEach(function(otherNode) { - if (otherNode !== targetNode && otherNode.parent === undefined) { - otherNode.parent = targetNode; - otherNode.isSelected = false; - self.arrRemove(self.selectedNodes, otherNode); - ops.push(new GroupOperation(targetNode, otherNode)); - } - }); - self.addUndoLogEntry(ops); - self.runLayout(); - }; - - this.ungroup = function(node) { - const ops = []; - self.nodes.forEach(function(other) { - if (other.parent === node) { - other.parent = undefined; - ops.push(new UnGroupOperation(node, other)); - } - }); - self.addUndoLogEntry(ops); - self.runLayout(); - }; - - this.unblacklist = function(node) { - self.arrRemove(self.blacklistedNodes, node); - }; - - this.blacklistSelection = function() { - const selection = self.getAllSelectedNodes(); - const danglingEdges = []; - self.edges.forEach(function(edge) { - if (selection.indexOf(edge.source) >= 0 || selection.indexOf(edge.target) >= 0) { - delete self.edgesMap[edge.id]; - danglingEdges.push(edge); - } - }); - selection.forEach(node => { - delete self.nodesMap[node.id]; - self.blacklistedNodes.push(node); - node.isSelected = false; - }); - self.arrRemoveAll(self.nodes, selection); - self.arrRemoveAll(self.edges, danglingEdges); - self.selectedNodes = []; - self.runLayout(); - }; - - // A "simple search" operation that requires no parameters from the client. - // Performs numHops hops pulling in field-specific number of terms each time - this.simpleSearch = function(searchTerm, fieldsChoice, numHops) { - const qs = { - query_string: { - query: searchTerm, - }, - }; - return this.search(qs, fieldsChoice, numHops); - }; + }); + self.selectNone(); + self.selectNode(node); + self.addUndoLogEntry(ops); + self.runLayout(); + }; - this.search = function(query, fieldsChoice, numHops) { - if (!fieldsChoice) { - fieldsChoice = self.options.vertex_fields; + this.mergeNeighbours = function(node) { + const neighbours = self.getNeighbours(node); + const ops = []; + neighbours.forEach(function(otherNode) { + if (otherNode !== node && otherNode.parent === undefined) { + otherNode.parent = node; + otherNode.isSelected = false; + self.arrRemove(self.selectedNodes, otherNode); + ops.push(new GroupOperation(node, otherNode)); } - let step = {}; - - //Add any blacklisted nodes to exclusion list - const excludeNodesByField = {}; - const nots = []; - const avoidNodes = this.blacklistedNodes; - for (let i = 0; i < avoidNodes.length; i++) { - const n = avoidNodes[i]; - let arr = excludeNodesByField[n.data.field]; - if (!arr) { - arr = []; - excludeNodesByField[n.data.field] = arr; - } - arr.push(n.data.term); - //Add to list of must_nots in guiding query - const tq = {}; - tq[n.data.field] = n.data.term; - nots.push({ - term: tq, - }); - } - - const rootStep = step; - for (let hopNum = 0; hopNum < numHops; hopNum++) { - const arr = []; + }); + self.addUndoLogEntry(ops); + self.runLayout(); + }; - fieldsChoice.forEach(({ name: field, hopSize }) => { - const excludes = excludeNodesByField[field]; - const stepField = { - field: field, - size: hopSize, - min_doc_count: parseInt(self.options.exploreControls.minDocCount), - }; - if (excludes) { - stepField.exclude = excludes; - } - arr.push(stepField); - }); - step.vertices = arr; - if (hopNum < numHops - 1) { - // if (s < (stepSizes.length - 1)) { - const nextStep = {}; - step.connections = nextStep; - step = nextStep; - } + this.mergeSelections = function(targetNode) { + if (!targetNode) { + console.log('Error - merge called on undefined target'); + return; + } + const selClone = self.selectedNodes.slice(); + const ops = []; + selClone.forEach(function(otherNode) { + if (otherNode !== targetNode && otherNode.parent === undefined) { + otherNode.parent = targetNode; + otherNode.isSelected = false; + self.arrRemove(self.selectedNodes, otherNode); + ops.push(new GroupOperation(targetNode, otherNode)); } + }); + self.addUndoLogEntry(ops); + self.runLayout(); + }; - if (nots.length > 0) { - query = { - bool: { - must: [query], - must_not: nots, - }, - }; + this.ungroup = function(node) { + const ops = []; + self.nodes.forEach(function(other) { + if (other.parent === node) { + other.parent = undefined; + ops.push(new UnGroupOperation(node, other)); } + }); + self.addUndoLogEntry(ops); + self.runLayout(); + }; - const request = { - query: query, - controls: self.buildControls(), - connections: rootStep.connections, - vertices: rootStep.vertices, - }; - self.callElasticsearch(request); - }; - - this.buildControls = function() { - //This is an object managed by the client that may be subject to change - const guiSettingsObj = self.options.exploreControls; + this.unblacklist = function(node) { + self.arrRemove(self.blacklistedNodes, node); + }; - const controls = { - use_significance: guiSettingsObj.useSignificance, - sample_size: guiSettingsObj.sampleSize, - timeout: parseInt(guiSettingsObj.timeoutMillis), - }; - // console.log("guiSettingsObj",guiSettingsObj); - if (guiSettingsObj.sampleDiversityField != null) { - controls.sample_diversity = { - field: guiSettingsObj.sampleDiversityField.name, - max_docs_per_value: guiSettingsObj.maxValuesPerDoc, - }; + this.blacklistSelection = function() { + const selection = self.getAllSelectedNodes(); + const danglingEdges = []; + self.edges.forEach(function(edge) { + if (selection.indexOf(edge.source) >= 0 || selection.indexOf(edge.target) >= 0) { + delete self.edgesMap[edge.id]; + danglingEdges.push(edge); } - return controls; - }; + }); + selection.forEach(node => { + delete self.nodesMap[node.id]; + self.blacklistedNodes.push(node); + node.isSelected = false; + }); + self.arrRemoveAll(self.nodes, selection); + self.arrRemoveAll(self.edges, danglingEdges); + self.selectedNodes = []; + self.runLayout(); + }; - this.makeNodeId = function(field, term) { - return field + '..' + term; + // A "simple search" operation that requires no parameters from the client. + // Performs numHops hops pulling in field-specific number of terms each time + this.simpleSearch = function(searchTerm, fieldsChoice, numHops) { + const qs = { + query_string: { + query: searchTerm, + }, }; + return this.search(qs, fieldsChoice, numHops); + }; - this.makeEdgeId = function(srcId, targetId) { - let id = srcId + '->' + targetId; - if (srcId > targetId) { - id = targetId + '->' + srcId; + this.search = function(query, fieldsChoice, numHops) { + if (!fieldsChoice) { + fieldsChoice = self.options.vertex_fields; + } + let step = {}; + + //Add any blacklisted nodes to exclusion list + const excludeNodesByField = {}; + const nots = []; + const avoidNodes = this.blacklistedNodes; + for (let i = 0; i < avoidNodes.length; i++) { + const n = avoidNodes[i]; + let arr = excludeNodesByField[n.data.field]; + if (!arr) { + arr = []; + excludeNodesByField[n.data.field] = arr; } - return id; - }; + arr.push(n.data.term); + //Add to list of must_nots in guiding query + const tq = {}; + tq[n.data.field] = n.data.term; + nots.push({ + term: tq, + }); + } - //======= Adds new nodes retrieved from an elasticsearch search ======== - this.mergeGraph = function(newData) { - this.stopLayout(); + const rootStep = step; + for (let hopNum = 0; hopNum < numHops; hopNum++) { + const arr = []; - if (!newData.nodes) { - newData.nodes = []; - } - const lastOps = []; - - // === Commented out - not sure it was obvious to users what various circle sizes meant - // var minCircleSize = 5; - // var maxCircleSize = 25; - // var sizeScale = d3.scale.pow().exponent(0.15) - // .domain([0, d3.max(newData.nodes, function(d) { - // return d.weight; - // })]) - // .range([minCircleSize, maxCircleSize]); - - //Remove nodes we already have - const dedupedNodes = []; - newData.nodes.forEach(node => { - //Assign an ID - node.id = self.makeNodeId(node.field, node.term); - if (!this.nodesMap[node.id]) { - //Default the label - if (!node.label) { - node.label = node.term; - } - dedupedNodes.push(node); + fieldsChoice.forEach(({ name: field, hopSize }) => { + const excludes = excludeNodesByField[field]; + const stepField = { + field: field, + size: hopSize, + min_doc_count: parseInt(self.options.exploreControls.minDocCount), + }; + if (excludes) { + stepField.exclude = excludes; } + arr.push(stepField); }); - if (dedupedNodes.length > 0 && this.options.nodeLabeller) { - // A hook for client code to attach labels etc to newly introduced nodes. - this.options.nodeLabeller(dedupedNodes); + step.vertices = arr; + if (hopNum < numHops - 1) { + // if (s < (stepSizes.length - 1)) { + const nextStep = {}; + step.connections = nextStep; + step = nextStep; } + } - dedupedNodes.forEach(dedupedNode => { - let label = dedupedNode.term; - if (dedupedNode.label) { - label = dedupedNode.label; - } + if (nots.length > 0) { + query = { + bool: { + must: [query], + must_not: nots, + }, + }; + } - const node = { - x: 1, - y: 1, - numChildren: 0, - parent: undefined, - isSelected: false, - id: dedupedNode.id, - label: label, - color: dedupedNode.color, - icon: dedupedNode.icon, - data: dedupedNode, - }; - // node.scaledSize = sizeScale(node.data.weight); - node.scaledSize = 15; - node.seqNumber = this.seqNumber++; - this.nodes.push(node); - lastOps.push(new AddNodeOperation(node, self)); - this.nodesMap[node.id] = node; - }); + const request = { + query: query, + controls: self.buildControls(), + connections: rootStep.connections, + vertices: rootStep.vertices, + }; + self.callElasticsearch(request); + }; - newData.edges.forEach(edge => { - const src = newData.nodes[edge.source]; - const target = newData.nodes[edge.target]; - edge.id = this.makeEdgeId(src.id, target.id); + this.buildControls = function() { + //This is an object managed by the client that may be subject to change + const guiSettingsObj = self.options.exploreControls; - //Lookup the wrappers object that will hold display Info like x/y coordinates - const srcWrapperObj = this.nodesMap[src.id]; - const targetWrapperObj = this.nodesMap[target.id]; + const controls = { + use_significance: guiSettingsObj.useSignificance, + sample_size: guiSettingsObj.sampleSize, + timeout: parseInt(guiSettingsObj.timeoutMillis), + }; + // console.log("guiSettingsObj",guiSettingsObj); + if (guiSettingsObj.sampleDiversityField != null) { + controls.sample_diversity = { + field: guiSettingsObj.sampleDiversityField.name, + max_docs_per_value: guiSettingsObj.maxValuesPerDoc, + }; + } + return controls; + }; - const existingEdge = this.edgesMap[edge.id]; - if (existingEdge) { - existingEdge.weight = Math.max(existingEdge.weight, edge.weight); - //TODO update width too? - existingEdge.doc_count = Math.max(existingEdge.doc_count, edge.doc_count); - return; - } - const newEdge = { - source: srcWrapperObj, - target: targetWrapperObj, - weight: edge.weight, - width: edge.width, - id: edge.id, - doc_count: edge.doc_count, - }; - if (edge.label) { - newEdge.label = edge.label; - } + this.makeNodeId = function(field, term) { + return field + '..' + term; + }; - this.edgesMap[newEdge.id] = newEdge; - this.edges.push(newEdge); - lastOps.push(new AddEdgeOperation(newEdge, self)); - }); + this.makeEdgeId = function(srcId, targetId) { + let id = srcId + '->' + targetId; + if (srcId > targetId) { + id = targetId + '->' + srcId; + } + return id; + }; + + //======= Adds new nodes retrieved from an elasticsearch search ======== + this.mergeGraph = function(newData) { + this.stopLayout(); + + if (!newData.nodes) { + newData.nodes = []; + } + const lastOps = []; + + // === Commented out - not sure it was obvious to users what various circle sizes meant + // var minCircleSize = 5; + // var maxCircleSize = 25; + // var sizeScale = d3.scale.pow().exponent(0.15) + // .domain([0, d3.max(newData.nodes, function(d) { + // return d.weight; + // })]) + // .range([minCircleSize, maxCircleSize]); + + //Remove nodes we already have + const dedupedNodes = []; + newData.nodes.forEach(node => { + //Assign an ID + node.id = self.makeNodeId(node.field, node.term); + if (!this.nodesMap[node.id]) { + //Default the label + if (!node.label) { + node.label = node.term; + } + dedupedNodes.push(node); + } + }); + if (dedupedNodes.length > 0 && this.options.nodeLabeller) { + // A hook for client code to attach labels etc to newly introduced nodes. + this.options.nodeLabeller(dedupedNodes); + } - if (lastOps.length > 0) { - self.addUndoLogEntry(lastOps); + dedupedNodes.forEach(dedupedNode => { + let label = dedupedNode.term; + if (dedupedNode.label) { + label = dedupedNode.label; } - this.runLayout(); - }; + const node = { + x: 1, + y: 1, + numChildren: 0, + parent: undefined, + isSelected: false, + id: dedupedNode.id, + label: label, + color: dedupedNode.color, + icon: dedupedNode.icon, + data: dedupedNode, + }; + // node.scaledSize = sizeScale(node.data.weight); + node.scaledSize = 15; + node.seqNumber = this.seqNumber++; + this.nodes.push(node); + lastOps.push(new AddNodeOperation(node, self)); + this.nodesMap[node.id] = node; + }); - this.mergeIds = function(parentId, childId) { - const parent = self.getNode(parentId); - const child = self.getNode(childId); - if (child.isSelected) { - child.isSelected = false; - self.arrRemove(self.selectedNodes, child); + newData.edges.forEach(edge => { + const src = newData.nodes[edge.source]; + const target = newData.nodes[edge.target]; + edge.id = this.makeEdgeId(src.id, target.id); + + //Lookup the wrappers object that will hold display Info like x/y coordinates + const srcWrapperObj = this.nodesMap[src.id]; + const targetWrapperObj = this.nodesMap[target.id]; + + const existingEdge = this.edgesMap[edge.id]; + if (existingEdge) { + existingEdge.weight = Math.max(existingEdge.weight, edge.weight); + //TODO update width too? + existingEdge.doc_count = Math.max(existingEdge.doc_count, edge.doc_count); + return; + } + const newEdge = { + source: srcWrapperObj, + target: targetWrapperObj, + weight: edge.weight, + width: edge.width, + id: edge.id, + doc_count: edge.doc_count, + }; + if (edge.label) { + newEdge.label = edge.label; } - child.parent = parent; - self.addUndoLogEntry([new GroupOperation(parent, child)]); - self.runLayout(); - }; - this.getNode = function(nodeId) { - return this.nodesMap[nodeId]; - }; - this.getEdge = function(edgeId) { - return this.edgesMap[edgeId]; - }; + this.edgesMap[newEdge.id] = newEdge; + this.edges.push(newEdge); + lastOps.push(new AddEdgeOperation(newEdge, self)); + }); - //======= Expand functions to request new additions to the graph + if (lastOps.length > 0) { + self.addUndoLogEntry(lastOps); + } - this.expandSelecteds = function(targetOptions = {}) { - let startNodes = self.getAllSelectedNodes(); - if (startNodes.length === 0) { - startNodes = self.nodes; - } - const clone = startNodes.slice(); - self.expand(clone, targetOptions); - }; + this.runLayout(); + }; - this.expandGraph = function() { - self.expandSelecteds(); - }; + this.mergeIds = function(parentId, childId) { + const parent = self.getNode(parentId); + const child = self.getNode(childId); + if (child.isSelected) { + child.isSelected = false; + self.arrRemove(self.selectedNodes, child); + } + child.parent = parent; + self.addUndoLogEntry([new GroupOperation(parent, child)]); + self.runLayout(); + }; - //Find new nodes to link to existing selected nodes - this.expandNode = function(node) { - self.expand(self.returnUnpackedGroupeds([node]), {}); - }; + this.getNode = function(nodeId) { + return this.nodesMap[nodeId]; + }; + this.getEdge = function(edgeId) { + return this.edgesMap[edgeId]; + }; - // A manual expand function where the client provides the list - // of existing nodes that are the start points and some options - // about what targets are of interest. - this.expand = function(startNodes, targetOptions) { - //============================= - const nodesByField = {}; - const excludeNodesByField = {}; - - //Add any blacklisted nodes to exclusion list - const avoidNodes = this.blacklistedNodes; - for (let i = 0; i < avoidNodes.length; i++) { - const n = avoidNodes[i]; - let arr = excludeNodesByField[n.data.field]; - if (!arr) { - arr = []; - excludeNodesByField[n.data.field] = arr; - } - if (arr.indexOf(n.data.term) < 0) { - arr.push(n.data.term); - } - } + //======= Expand functions to request new additions to the graph - const allExistingNodes = this.nodes; - for (let i = 0; i < allExistingNodes.length; i++) { - const n = allExistingNodes[i]; - let arr = excludeNodesByField[n.data.field]; - if (!arr) { - arr = []; - excludeNodesByField[n.data.field] = arr; - } - arr.push(n.data.term); - } + this.expandSelecteds = function(targetOptions = {}) { + let startNodes = self.getAllSelectedNodes(); + if (startNodes.length === 0) { + startNodes = self.nodes; + } + const clone = startNodes.slice(); + self.expand(clone, targetOptions); + }; - //Organize nodes by field - for (let i = 0; i < startNodes.length; i++) { - const n = startNodes[i]; - let arr = nodesByField[n.data.field]; - if (!arr) { - arr = []; - nodesByField[n.data.field] = arr; - } - // pushing boosts server-side to influence sampling/direction - arr.push({ - term: n.data.term, - boost: n.data.weight, - }); + this.expandGraph = function() { + self.expandSelecteds(); + }; - arr = excludeNodesByField[n.data.field]; - if (!arr) { - arr = []; - excludeNodesByField[n.data.field] = arr; - } - //NOTE for the entity-building use case need to remove excludes that otherwise - // prevent bridge-building. - if (arr.indexOf(n.data.term) < 0) { - arr.push(n.data.term); - } + //Find new nodes to link to existing selected nodes + this.expandNode = function(node) { + self.expand(self.returnUnpackedGroupeds([node]), {}); + }; + + // A manual expand function where the client provides the list + // of existing nodes that are the start points and some options + // about what targets are of interest. + this.expand = function(startNodes, targetOptions) { + //============================= + const nodesByField = {}; + const excludeNodesByField = {}; + + //Add any blacklisted nodes to exclusion list + const avoidNodes = this.blacklistedNodes; + for (let i = 0; i < avoidNodes.length; i++) { + const n = avoidNodes[i]; + let arr = excludeNodesByField[n.data.field]; + if (!arr) { + arr = []; + excludeNodesByField[n.data.field] = arr; } + if (arr.indexOf(n.data.term) < 0) { + arr.push(n.data.term); + } + } - const primaryVertices = []; - const secondaryVertices = []; - for (const fieldName in nodesByField) { - if (nodesByField.hasOwnProperty(fieldName)) { - primaryVertices.push({ - field: fieldName, - include: nodesByField[fieldName], - min_doc_count: parseInt(self.options.exploreControls.minDocCount), - }); - } + const allExistingNodes = this.nodes; + for (let i = 0; i < allExistingNodes.length; i++) { + const n = allExistingNodes[i]; + let arr = excludeNodesByField[n.data.field]; + if (!arr) { + arr = []; + excludeNodesByField[n.data.field] = arr; } + arr.push(n.data.term); + } - let targetFields = this.options.vertex_fields; - if (targetOptions.toFields) { - targetFields = targetOptions.toFields; + //Organize nodes by field + for (let i = 0; i < startNodes.length; i++) { + const n = startNodes[i]; + let arr = nodesByField[n.data.field]; + if (!arr) { + arr = []; + nodesByField[n.data.field] = arr; } + // pushing boosts server-side to influence sampling/direction + arr.push({ + term: n.data.term, + boost: n.data.weight, + }); - //Identify target fields - targetFields.forEach(targetField => { - const fieldName = targetField.name; - // Sometimes the target field is disabled from loading new hops so we need to use the last valid figure - const hopSize = - targetField.hopSize > 0 ? targetField.hopSize : targetField.lastValidHopSize; + arr = excludeNodesByField[n.data.field]; + if (!arr) { + arr = []; + excludeNodesByField[n.data.field] = arr; + } + //NOTE for the entity-building use case need to remove excludes that otherwise + // prevent bridge-building. + if (arr.indexOf(n.data.term) < 0) { + arr.push(n.data.term); + } + } - const fieldHop = { + const primaryVertices = []; + const secondaryVertices = []; + for (const fieldName in nodesByField) { + if (nodesByField.hasOwnProperty(fieldName)) { + primaryVertices.push({ field: fieldName, - size: hopSize, + include: nodesByField[fieldName], min_doc_count: parseInt(self.options.exploreControls.minDocCount), - }; - fieldHop.exclude = excludeNodesByField[fieldName]; - secondaryVertices.push(fieldHop); - }); + }); + } + } - const request = { - controls: self.buildControls(), - vertices: primaryVertices, - connections: { - vertices: secondaryVertices, - }, + let targetFields = this.options.vertex_fields; + if (targetOptions.toFields) { + targetFields = targetOptions.toFields; + } + + //Identify target fields + targetFields.forEach(targetField => { + const fieldName = targetField.name; + // Sometimes the target field is disabled from loading new hops so we need to use the last valid figure + const hopSize = targetField.hopSize > 0 ? targetField.hopSize : targetField.lastValidHopSize; + + const fieldHop = { + field: fieldName, + size: hopSize, + min_doc_count: parseInt(self.options.exploreControls.minDocCount), }; - self.lastRequest = JSON.stringify(request, null, '\t'); - graphExplorer(self.options.indexName, request, function(data) { - self.lastResponse = JSON.stringify(data, null, '\t'); - const edges = []; - - //Label fields with a field number for CSS styling - data.vertices.forEach(node => { - targetFields.some(fieldDef => { - if (node.field === fieldDef.name) { - node.color = fieldDef.color; - node.icon = fieldDef.icon; - node.fieldDef = fieldDef; - return true; - } - return false; - }); - }); + fieldHop.exclude = excludeNodesByField[fieldName]; + secondaryVertices.push(fieldHop); + }); - // Size the edges based on the maximum weight - const minLineSize = 2; - const maxLineSize = 10; - let maxEdgeWeight = 0.00000001; - data.connections.forEach(edge => { - maxEdgeWeight = Math.max(maxEdgeWeight, edge.weight); - edges.push({ - source: edge.source, - target: edge.target, - doc_count: edge.doc_count, - weight: edge.weight, - width: Math.max(minLineSize, (edge.weight / maxEdgeWeight) * maxLineSize), - }); + const request = { + controls: self.buildControls(), + vertices: primaryVertices, + connections: { + vertices: secondaryVertices, + }, + }; + self.lastRequest = JSON.stringify(request, null, '\t'); + graphExplorer(self.options.indexName, request, function(data) { + self.lastResponse = JSON.stringify(data, null, '\t'); + const edges = []; + + //Label fields with a field number for CSS styling + data.vertices.forEach(node => { + targetFields.some(fieldDef => { + if (node.field === fieldDef.name) { + node.color = fieldDef.color; + node.icon = fieldDef.icon; + node.fieldDef = fieldDef; + return true; + } + return false; }); + }); - // Add the new nodes and edges into the existing workspace's graph - self.mergeGraph({ - nodes: data.vertices, - edges: edges, + // Size the edges based on the maximum weight + const minLineSize = 2; + const maxLineSize = 10; + let maxEdgeWeight = 0.00000001; + data.connections.forEach(edge => { + maxEdgeWeight = Math.max(maxEdgeWeight, edge.weight); + edges.push({ + source: edge.source, + target: edge.target, + doc_count: edge.doc_count, + weight: edge.weight, + width: Math.max(minLineSize, (edge.weight / maxEdgeWeight) * maxLineSize), }); }); - //===== End expand graph ======================== - }; - this.trimExcessNewEdges = function(newNodes, newEdges) { - let trimmedEdges = []; - const maxNumEdgesToReturn = 5; - //Trim here to just the new edges that are most interesting. - newEdges.forEach(edge => { - const src = newNodes[edge.source]; - const target = newNodes[edge.target]; - const srcId = src.field + '..' + src.term; - const targetId = target.field + '..' + target.term; - const id = this.makeEdgeId(srcId, targetId); - const existingSrcNode = self.nodesMap[srcId]; - const existingTargetNode = self.nodesMap[targetId]; - if (existingSrcNode != null && existingTargetNode != null) { - if (existingSrcNode.parent !== undefined && existingTargetNode.parent !== undefined) { - // both nodes are rolled-up and grouped so this edge would not be a visible - // change to the graph - lose it in favour of any other visible ones. - return; - } - } else { - console.log('Error? Missing nodes ' + srcId + ' or ' + targetId, self.nodesMap); - return; - } + // Add the new nodes and edges into the existing workspace's graph + self.mergeGraph({ + nodes: data.vertices, + edges: edges, + }); + }); + //===== End expand graph ======================== + }; - const existingEdge = self.edgesMap[id]; - if (existingEdge) { - existingEdge.weight = Math.max(existingEdge.weight, edge.weight); - existingEdge.doc_count = Math.max(existingEdge.doc_count, edge.doc_count); + this.trimExcessNewEdges = function(newNodes, newEdges) { + let trimmedEdges = []; + const maxNumEdgesToReturn = 5; + //Trim here to just the new edges that are most interesting. + newEdges.forEach(edge => { + const src = newNodes[edge.source]; + const target = newNodes[edge.target]; + const srcId = src.field + '..' + src.term; + const targetId = target.field + '..' + target.term; + const id = this.makeEdgeId(srcId, targetId); + const existingSrcNode = self.nodesMap[srcId]; + const existingTargetNode = self.nodesMap[targetId]; + if (existingSrcNode != null && existingTargetNode != null) { + if (existingSrcNode.parent !== undefined && existingTargetNode.parent !== undefined) { + // both nodes are rolled-up and grouped so this edge would not be a visible + // change to the graph - lose it in favour of any other visible ones. return; - } else { - trimmedEdges.push(edge); } - }); - if (trimmedEdges.length > maxNumEdgesToReturn) { - //trim to only the most interesting ones - trimmedEdges.sort(function(a, b) { - return b.weight - a.weight; - }); - trimmedEdges = trimmedEdges.splice(0, maxNumEdgesToReturn); + } else { + console.log('Error? Missing nodes ' + srcId + ' or ' + targetId, self.nodesMap); + return; } - return trimmedEdges; - }; - this.getQuery = function(startNodes, loose) { - const shoulds = []; - let nodes = startNodes; - if (!startNodes) { - nodes = self.nodes; + const existingEdge = self.edgesMap[id]; + if (existingEdge) { + existingEdge.weight = Math.max(existingEdge.weight, edge.weight); + existingEdge.doc_count = Math.max(existingEdge.doc_count, edge.doc_count); + return; + } else { + trimmedEdges.push(edge); } - nodes.forEach(node => { - if (node.parent === undefined) { - shoulds.push(self.buildNodeQuery(node)); - } + }); + if (trimmedEdges.length > maxNumEdgesToReturn) { + //trim to only the most interesting ones + trimmedEdges.sort(function(a, b) { + return b.weight - a.weight; }); - return { - bool: { - should: shoulds, - minimum_should_match: Math.min(shoulds.length, loose ? 1 : 2), - }, - }; - }; + trimmedEdges = trimmedEdges.splice(0, maxNumEdgesToReturn); + } + return trimmedEdges; + }; - this.getSelectedOrAllNodes = function() { - let startNodes = self.getAllSelectedNodes(); - if (startNodes.length === 0) { - startNodes = self.nodes; + this.getQuery = function(startNodes, loose) { + const shoulds = []; + let nodes = startNodes; + if (!startNodes) { + nodes = self.nodes; + } + nodes.forEach(node => { + if (node.parent === undefined) { + shoulds.push(self.buildNodeQuery(node)); } - return startNodes; + }); + return { + bool: { + should: shoulds, + minimum_should_match: Math.min(shoulds.length, loose ? 1 : 2), + }, }; + }; - this.getSelectedOrAllTopNodes = function() { - return self.getSelectedOrAllNodes().filter(function(node) { - return node.parent === undefined; - }); - }; + this.getSelectedOrAllNodes = function() { + let startNodes = self.getAllSelectedNodes(); + if (startNodes.length === 0) { + startNodes = self.nodes; + } + return startNodes; + }; - function addTermToFieldList(map, field, term) { - let arr = map[field]; - if (!arr) { - arr = []; - map[field] = arr; - } - arr.push(term); + this.getSelectedOrAllTopNodes = function() { + return self.getSelectedOrAllNodes().filter(function(node) { + return node.parent === undefined; + }); + }; + + function addTermToFieldList(map, field, term) { + let arr = map[field]; + if (!arr) { + arr = []; + map[field] = arr; } + arr.push(term); + } - /** - * Add missing links between existing nodes - * @param maxNewEdges Max number of new edges added. Avoid adding too many new edges - * at once into the graph otherwise disorientating - */ - this.fillInGraph = function(maxNewEdges = 10) { - let nodesForLinking = self.getSelectedOrAllTopNodes(); - - const maxNumVerticesSearchable = 100; - - // Server limitation - we can only search for connections between max 100 vertices at a time. - if (nodesForLinking.length > maxNumVerticesSearchable) { - //Make a selection of random nodes from the array. Shift the random choices - // to the front of the array. - for (let i = 0; i < maxNumVerticesSearchable; i++) { - const oldNode = nodesForLinking[i]; - const randomIndex = Math.floor(Math.random() * (nodesForLinking.length - i)) + i; - //Swap the node positions of the randomly selected node and i - nodesForLinking[i] = nodesForLinking[randomIndex]; - nodesForLinking[randomIndex] = oldNode; - } - // Trim to our random selection - nodesForLinking = nodesForLinking.slice(0, maxNumVerticesSearchable - 1); + /** + * Add missing links between existing nodes + * @param maxNewEdges Max number of new edges added. Avoid adding too many new edges + * at once into the graph otherwise disorientating + */ + this.fillInGraph = function(maxNewEdges = 10) { + let nodesForLinking = self.getSelectedOrAllTopNodes(); + + const maxNumVerticesSearchable = 100; + + // Server limitation - we can only search for connections between max 100 vertices at a time. + if (nodesForLinking.length > maxNumVerticesSearchable) { + //Make a selection of random nodes from the array. Shift the random choices + // to the front of the array. + for (let i = 0; i < maxNumVerticesSearchable; i++) { + const oldNode = nodesForLinking[i]; + const randomIndex = Math.floor(Math.random() * (nodesForLinking.length - i)) + i; + //Swap the node positions of the randomly selected node and i + nodesForLinking[i] = nodesForLinking[randomIndex]; + nodesForLinking[randomIndex] = oldNode; } + // Trim to our random selection + nodesForLinking = nodesForLinking.slice(0, maxNumVerticesSearchable - 1); + } - // Create our query/aggregation request using the selected nodes. - // Filters are named after the index of the node in the nodesForLinking - // array. The result bucket describing the relationship between - // the first 2 nodes in the array will therefore be labelled "0|1" - const shoulds = []; - const filterMap = {}; - nodesForLinking.forEach(function(node, nodeNum) { - const nodeQuery = self.buildNodeQuery(node); - shoulds.push(nodeQuery); - filterMap[nodeNum] = nodeQuery; - }); - const searchReq = { - size: 0, - query: { - bool: { - // Only match docs that share 2 nodes so can help describe their relationship - minimum_should_match: 2, - should: shoulds, - }, + // Create our query/aggregation request using the selected nodes. + // Filters are named after the index of the node in the nodesForLinking + // array. The result bucket describing the relationship between + // the first 2 nodes in the array will therefore be labelled "0|1" + const shoulds = []; + const filterMap = {}; + nodesForLinking.forEach(function(node, nodeNum) { + const nodeQuery = self.buildNodeQuery(node); + shoulds.push(nodeQuery); + filterMap[nodeNum] = nodeQuery; + }); + const searchReq = { + size: 0, + query: { + bool: { + // Only match docs that share 2 nodes so can help describe their relationship + minimum_should_match: 2, + should: shoulds, }, - aggs: { - matrix: { - adjacency_matrix: { - separator: '|', - filters: filterMap, - }, + }, + aggs: { + matrix: { + adjacency_matrix: { + separator: '|', + filters: filterMap, }, }, - }; + }, + }; - // Search for connections between the selected nodes. - searcher(self.options.indexName, searchReq, function(data) { - const numDocsMatched = data.hits.total; - const buckets = data.aggregations.matrix.buckets; - const vertices = nodesForLinking.map(function(existingNode) { - return { - field: existingNode.data.field, - term: existingNode.data.term, - weight: 1, - depth: 0, - }; - }); + // Search for connections between the selected nodes. + searcher(self.options.indexName, searchReq, function(data) { + const numDocsMatched = data.hits.total; + const buckets = data.aggregations.matrix.buckets; + const vertices = nodesForLinking.map(function(existingNode) { + return { + field: existingNode.data.field, + term: existingNode.data.term, + weight: 1, + depth: 0, + }; + }); - let connections = []; - let maxEdgeWeight = 0; - // Turn matrix array of results into a map - const keyedBuckets = {}; - buckets.forEach(function(bucket) { - keyedBuckets[bucket.key] = bucket; - }); + let connections = []; + let maxEdgeWeight = 0; + // Turn matrix array of results into a map + const keyedBuckets = {}; + buckets.forEach(function(bucket) { + keyedBuckets[bucket.key] = bucket; + }); - buckets.forEach(function(bucket) { - // We calibrate line thickness based on % of max weight of - // all edges (including the edges we may already have in the workspace) - const ids = bucket.key.split('|'); - if (ids.length === 2) { - // bucket represents an edge - if (self.options.exploreControls.useSignificance) { - const t1 = keyedBuckets[ids[0]].doc_count; - const t2 = keyedBuckets[ids[1]].doc_count; - const t1AndT2 = bucket.doc_count; - // Calc the significant_terms score to prioritize selection of interesting links - bucket.weight = self.jLHScore( - t1AndT2, - Math.max(t1, t2), - Math.min(t1, t2), - numDocsMatched - ); - } else { - // prioritize links purely on volume of intersecting docs - bucket.weight = bucket.doc_count; - } - maxEdgeWeight = Math.max(maxEdgeWeight, bucket.weight); - } - }); - const backFilledMinLineSize = 2; - const backFilledMaxLineSize = 5; - buckets.forEach(function(bucket) { - if (bucket.doc_count < parseInt(self.options.exploreControls.minDocCount)) { - return; - } - const ids = bucket.key.split('|'); - if (ids.length === 2) { - // Bucket represents an edge - const srcNode = nodesForLinking[ids[0]]; - const targetNode = nodesForLinking[ids[1]]; - const edgeId = self.makeEdgeId(srcNode.id, targetNode.id); - const existingEdge = self.edgesMap[edgeId]; - if (existingEdge) { - // Tweak the doc_count score having just looked it up. - existingEdge.doc_count = Math.max(existingEdge.doc_count, bucket.doc_count); - } else { - connections.push({ - // source and target values are indexes into the vertices array - source: parseInt(ids[0]), - target: parseInt(ids[1]), - weight: bucket.weight, - width: Math.max( - backFilledMinLineSize, - (bucket.weight / maxEdgeWeight) * backFilledMaxLineSize - ), - doc_count: bucket.doc_count, - }); - } + buckets.forEach(function(bucket) { + // We calibrate line thickness based on % of max weight of + // all edges (including the edges we may already have in the workspace) + const ids = bucket.key.split('|'); + if (ids.length === 2) { + // bucket represents an edge + if (self.options.exploreControls.useSignificance) { + const t1 = keyedBuckets[ids[0]].doc_count; + const t2 = keyedBuckets[ids[1]].doc_count; + const t1AndT2 = bucket.doc_count; + // Calc the significant_terms score to prioritize selection of interesting links + bucket.weight = self.jLHScore( + t1AndT2, + Math.max(t1, t2), + Math.min(t1, t2), + numDocsMatched + ); + } else { + // prioritize links purely on volume of intersecting docs + bucket.weight = bucket.doc_count; } - }); - // Trim the array of connections so that we don't add too many at once - disorientating for users otherwise - if (connections.length > maxNewEdges) { - connections = connections.sort(function(a, b) { - return b.weight - a.weight; - }); - connections = connections.slice(0, maxNewEdges); + maxEdgeWeight = Math.max(maxEdgeWeight, bucket.weight); } - - // Merge the new edges into the existing workspace's graph. - // We reuse the mergeGraph function used to handle the - // results of other calls to the server-side Graph API - // so must package the results here with that same format - // even though we know all the vertices we provide will - // be duplicates and ignored. - self.mergeGraph({ - nodes: vertices, - edges: connections, - }); }); - }; - - // Provide a "fuzzy find similar" query that can find similar docs but preferably - // not re-iterating the exact terms we already have in the workspace. - // We use a free-text search on the index's configured default field (typically '_all') - // to drill-down into docs that should be linked but aren't via the exact terms - // we have in the workspace - this.getLikeThisButNotThisQuery = function(startNodes) { - const likeQueries = []; - - const txtsByFieldType = {}; - startNodes.forEach(node => { - let txt = txtsByFieldType[node.data.field]; - if (txt) { - txt = txt + ' ' + node.label; - } else { - txt = node.label; + const backFilledMinLineSize = 2; + const backFilledMaxLineSize = 5; + buckets.forEach(function(bucket) { + if (bucket.doc_count < parseInt(self.options.exploreControls.minDocCount)) { + return; } - txtsByFieldType[node.data.field] = txt; - }); - for (const field in txtsByFieldType) { - if (txtsByFieldType.hasOwnProperty(field)) { - likeQueries.push({ - more_like_this: { - like: txtsByFieldType[field], - min_term_freq: 1, - minimum_should_match: '20%', - min_doc_freq: 1, - boost_terms: 2, - max_query_terms: 25, - }, - }); + const ids = bucket.key.split('|'); + if (ids.length === 2) { + // Bucket represents an edge + const srcNode = nodesForLinking[ids[0]]; + const targetNode = nodesForLinking[ids[1]]; + const edgeId = self.makeEdgeId(srcNode.id, targetNode.id); + const existingEdge = self.edgesMap[edgeId]; + if (existingEdge) { + // Tweak the doc_count score having just looked it up. + existingEdge.doc_count = Math.max(existingEdge.doc_count, bucket.doc_count); + } else { + connections.push({ + // source and target values are indexes into the vertices array + source: parseInt(ids[0]), + target: parseInt(ids[1]), + weight: bucket.weight, + width: Math.max( + backFilledMinLineSize, + (bucket.weight / maxEdgeWeight) * backFilledMaxLineSize + ), + doc_count: bucket.doc_count, + }); + } } + }); + // Trim the array of connections so that we don't add too many at once - disorientating for users otherwise + if (connections.length > maxNewEdges) { + connections = connections.sort(function(a, b) { + return b.weight - a.weight; + }); + connections = connections.slice(0, maxNewEdges); } - const excludeNodesByField = {}; - const allExistingNodes = self.nodes; - allExistingNodes.forEach(existingNode => { - addTermToFieldList(excludeNodesByField, existingNode.data.field, existingNode.data.term); - }); - const blacklistedNodes = self.blacklistedNodes; - blacklistedNodes.forEach(blacklistedNode => { - addTermToFieldList( - excludeNodesByField, - blacklistedNode.data.field, - blacklistedNode.data.term - ); + // Merge the new edges into the existing workspace's graph. + // We reuse the mergeGraph function used to handle the + // results of other calls to the server-side Graph API + // so must package the results here with that same format + // even though we know all the vertices we provide will + // be duplicates and ignored. + self.mergeGraph({ + nodes: vertices, + edges: connections, }); + }); + }; - //Create negative boosting queries to avoid matching what you already have in the workspace. - const notExistingNodes = []; - Object.keys(excludeNodesByField).forEach(fieldName => { - const termsQuery = {}; - termsQuery[fieldName] = excludeNodesByField[fieldName]; - notExistingNodes.push({ - terms: termsQuery, + // Provide a "fuzzy find similar" query that can find similar docs but preferably + // not re-iterating the exact terms we already have in the workspace. + // We use a free-text search on the index's configured default field (typically '_all') + // to drill-down into docs that should be linked but aren't via the exact terms + // we have in the workspace + this.getLikeThisButNotThisQuery = function(startNodes) { + const likeQueries = []; + + const txtsByFieldType = {}; + startNodes.forEach(node => { + let txt = txtsByFieldType[node.data.field]; + if (txt) { + txt = txt + ' ' + node.label; + } else { + txt = node.label; + } + txtsByFieldType[node.data.field] = txt; + }); + for (const field in txtsByFieldType) { + if (txtsByFieldType.hasOwnProperty(field)) { + likeQueries.push({ + more_like_this: { + like: txtsByFieldType[field], + min_term_freq: 1, + minimum_should_match: '20%', + min_doc_freq: 1, + boost_terms: 2, + max_query_terms: 25, + }, }); + } + } + + const excludeNodesByField = {}; + const allExistingNodes = self.nodes; + allExistingNodes.forEach(existingNode => { + addTermToFieldList(excludeNodesByField, existingNode.data.field, existingNode.data.term); + }); + const blacklistedNodes = self.blacklistedNodes; + blacklistedNodes.forEach(blacklistedNode => { + addTermToFieldList( + excludeNodesByField, + blacklistedNode.data.field, + blacklistedNode.data.term + ); + }); + + //Create negative boosting queries to avoid matching what you already have in the workspace. + const notExistingNodes = []; + Object.keys(excludeNodesByField).forEach(fieldName => { + const termsQuery = {}; + termsQuery[fieldName] = excludeNodesByField[fieldName]; + notExistingNodes.push({ + terms: termsQuery, }); + }); - const result = { - // Use a boosting query to effectively to request "similar to these IDS/labels but - // preferably not containing these exact IDs". - boosting: { - negative_boost: 0.0001, - negative: { - bool: { - should: notExistingNodes, - }, + const result = { + // Use a boosting query to effectively to request "similar to these IDS/labels but + // preferably not containing these exact IDs". + boosting: { + negative_boost: 0.0001, + negative: { + bool: { + should: notExistingNodes, }, - positive: { - bool: { - should: likeQueries, - }, + }, + positive: { + bool: { + should: likeQueries, }, }, - }; - return result; + }, }; + return result; + }; - this.getSelectedIntersections = function(callback) { - if (self.selectedNodes.length === 0) { - return self.getAllIntersections(callback, self.nodes); - } - if (self.selectedNodes.length === 1) { - const selectedNode = self.selectedNodes[0]; - const neighbourNodes = self.getNeighbours(selectedNode); - neighbourNodes.push(selectedNode); - return self.getAllIntersections(callback, neighbourNodes); - } - return self.getAllIntersections(callback, self.getAllSelectedNodes()); - }; + this.getSelectedIntersections = function(callback) { + if (self.selectedNodes.length === 0) { + return self.getAllIntersections(callback, self.nodes); + } + if (self.selectedNodes.length === 1) { + const selectedNode = self.selectedNodes[0]; + const neighbourNodes = self.getNeighbours(selectedNode); + neighbourNodes.push(selectedNode); + return self.getAllIntersections(callback, neighbourNodes); + } + return self.getAllIntersections(callback, self.getAllSelectedNodes()); + }; - this.jLHScore = function(subsetFreq, subsetSize, supersetFreq, supersetSize) { - const subsetProbability = subsetFreq / subsetSize; - const supersetProbability = supersetFreq / supersetSize; + this.jLHScore = function(subsetFreq, subsetSize, supersetFreq, supersetSize) { + const subsetProbability = subsetFreq / subsetSize; + const supersetProbability = supersetFreq / supersetSize; - const absoluteProbabilityChange = subsetProbability - supersetProbability; - if (absoluteProbabilityChange <= 0) { - return 0; - } - const relativeProbabilityChange = subsetProbability / supersetProbability; - return absoluteProbabilityChange * relativeProbabilityChange; - }; + const absoluteProbabilityChange = subsetProbability - supersetProbability; + if (absoluteProbabilityChange <= 0) { + return 0; + } + const relativeProbabilityChange = subsetProbability / supersetProbability; + return absoluteProbabilityChange * relativeProbabilityChange; + }; - // Currently unused in the Kibana UI. It was a utility that provided a sorted list - // of recommended node merges for a selection of nodes. Top results would be - // rare nodes that ALWAYS appear alongside more popular ones e.g. text:9200 always - // appears alongside hashtag:elasticsearch so would be offered as a likely candidate - // for merging. - - // Determines union/intersection stats for neighbours of a node. - // TODO - could move server-side as a graph API function? - this.getAllIntersections = function(callback, nodes) { - //Ensure these are all top-level nodes only - nodes = nodes.filter(function(n) { - return n.parent === undefined; - }); + // Currently unused in the Kibana UI. It was a utility that provided a sorted list + // of recommended node merges for a selection of nodes. Top results would be + // rare nodes that ALWAYS appear alongside more popular ones e.g. text:9200 always + // appears alongside hashtag:elasticsearch so would be offered as a likely candidate + // for merging. + + // Determines union/intersection stats for neighbours of a node. + // TODO - could move server-side as a graph API function? + this.getAllIntersections = function(callback, nodes) { + //Ensure these are all top-level nodes only + nodes = nodes.filter(function(n) { + return n.parent === undefined; + }); - const allQueries = nodes.map(function(node) { - return self.buildNodeQuery(node); - }); + const allQueries = nodes.map(function(node) { + return self.buildNodeQuery(node); + }); - const allQuery = { - bool: { - should: allQueries, + const allQuery = { + bool: { + should: allQueries, + }, + }; + //==================== + const request = { + query: allQuery, + size: 0, + aggs: { + all: { + global: {}, }, - }; - //==================== - const request = { - query: allQuery, - size: 0, - aggs: { - all: { - global: {}, + sources: { + // Could use significant_terms not filters to get stats but + // for the fact some of the nodes are groups of terms. + filters: { + filters: {}, }, - sources: { - // Could use significant_terms not filters to get stats but - // for the fact some of the nodes are groups of terms. - filters: { - filters: {}, - }, - aggs: { - targets: { - filters: { - filters: {}, - }, + aggs: { + targets: { + filters: { + filters: {}, }, }, }, }, - }; - allQueries.forEach((query, n) => { - // Add aggs to get intersection stats with root node. - request.aggs.sources.filters.filters['bg' + n] = query; - request.aggs.sources.aggs.targets.filters.filters['fg' + n] = query; + }, + }; + allQueries.forEach((query, n) => { + // Add aggs to get intersection stats with root node. + request.aggs.sources.filters.filters['bg' + n] = query; + request.aggs.sources.aggs.targets.filters.filters['fg' + n] = query; + }); + searcher(self.options.indexName, request, function(data) { + const termIntersects = []; + const fullDocCounts = []; + const allDocCount = data.aggregations.all.doc_count; + + // Gather the background stats for all nodes. + nodes.forEach((rootNode, n) => { + fullDocCounts.push(data.aggregations.sources.buckets['bg' + n].doc_count); }); - searcher(self.options.indexName, request, function(data) { - const termIntersects = []; - const fullDocCounts = []; - const allDocCount = data.aggregations.all.doc_count; - - // Gather the background stats for all nodes. - nodes.forEach((rootNode, n) => { - fullDocCounts.push(data.aggregations.sources.buckets['bg' + n].doc_count); - }); - nodes.forEach((rootNode, n) => { - const t1 = fullDocCounts[n]; - const baseAgg = data.aggregations.sources.buckets['bg' + n].targets.buckets; - nodes.forEach((leafNode, l) => { - const t2 = fullDocCounts[l]; - if (l === n) { - return; - } - if (t1 > t2) { + nodes.forEach((rootNode, n) => { + const t1 = fullDocCounts[n]; + const baseAgg = data.aggregations.sources.buckets['bg' + n].targets.buckets; + nodes.forEach((leafNode, l) => { + const t2 = fullDocCounts[l]; + if (l === n) { + return; + } + if (t1 > t2) { + // We should get the same stats for t2->t1 from the t1->t2 bucket path + return; + } + if (t1 === t2) { + if (rootNode.id > leafNode.id) { // We should get the same stats for t2->t1 from the t1->t2 bucket path return; } - if (t1 === t2) { - if (rootNode.id > leafNode.id) { - // We should get the same stats for t2->t1 from the t1->t2 bucket path - return; - } - } - const t1AndT2 = baseAgg['fg' + l].doc_count; - if (t1AndT2 === 0) { - return; - } - const neighbourNode = nodes[l]; - let t1Label = rootNode.data.label; - if (rootNode.numChildren > 0) { - t1Label += '(+' + rootNode.numChildren + ')'; - } - let t2Label = neighbourNode.data.label; - if (neighbourNode.numChildren > 0) { - t2Label += '(+' + neighbourNode.numChildren + ')'; - } - - // A straight percentage can be poor if t1==1 (100%) - not too much strength of evidence - // var mergeConfidence=t1AndT2/t1; - - // So using Significance heuristic instead - const mergeConfidence = self.jLHScore(t1AndT2, t2, t1, allDocCount); - - const termIntersect = { - id1: rootNode.id, - id2: neighbourNode.id, - term1: t1Label, - term2: t2Label, - v1: t1, - v2: t2, - mergeLeftConfidence: t1AndT2 / t1, - mergeRightConfidence: t1AndT2 / t2, - mergeConfidence: mergeConfidence, - overlap: t1AndT2, - }; - termIntersects.push(termIntersect); - }); - }); - termIntersects.sort(function(a, b) { - if (b.mergeConfidence !== a.mergeConfidence) { - return b.mergeConfidence - a.mergeConfidence; } - // If of equal similarity use the size of the overlap as - // a measure of magnitude/significance for tie-breaker. - - if (b.overlap !== a.overlap) { - return b.overlap - a.overlap; + const t1AndT2 = baseAgg['fg' + l].doc_count; + if (t1AndT2 === 0) { + return; + } + const neighbourNode = nodes[l]; + let t1Label = rootNode.data.label; + if (rootNode.numChildren > 0) { + t1Label += '(+' + rootNode.numChildren + ')'; } - //All other things being equal we now favour where t2 NOT t1 is small. - return a.v2 - b.v2; + let t2Label = neighbourNode.data.label; + if (neighbourNode.numChildren > 0) { + t2Label += '(+' + neighbourNode.numChildren + ')'; + } + + // A straight percentage can be poor if t1==1 (100%) - not too much strength of evidence + // var mergeConfidence=t1AndT2/t1; + + // So using Significance heuristic instead + const mergeConfidence = self.jLHScore(t1AndT2, t2, t1, allDocCount); + + const termIntersect = { + id1: rootNode.id, + id2: neighbourNode.id, + term1: t1Label, + term2: t2Label, + v1: t1, + v2: t2, + mergeLeftConfidence: t1AndT2 / t1, + mergeRightConfidence: t1AndT2 / t2, + mergeConfidence: mergeConfidence, + overlap: t1AndT2, + }; + termIntersects.push(termIntersect); }); - if (callback) { - callback(termIntersects); + }); + termIntersects.sort(function(a, b) { + if (b.mergeConfidence !== a.mergeConfidence) { + return b.mergeConfidence - a.mergeConfidence; + } + // If of equal similarity use the size of the overlap as + // a measure of magnitude/significance for tie-breaker. + + if (b.overlap !== a.overlap) { + return b.overlap - a.overlap; } + //All other things being equal we now favour where t2 NOT t1 is small. + return a.v2 - b.v2; }); - }; + if (callback) { + callback(termIntersects); + } + }); + }; - // Internal utility function for calling the Graph API and handling the response - // by merging results into existing nodes in this workspace. - this.callElasticsearch = function(request) { - self.lastRequest = JSON.stringify(request, null, '\t'); - graphExplorer(self.options.indexName, request, function(data) { - self.lastResponse = JSON.stringify(data, null, '\t'); - const edges = []; - //Label the nodes with field number for CSS styling - data.vertices.forEach(node => { - self.options.vertex_fields.some(fieldDef => { - if (node.field === fieldDef.name) { - node.color = fieldDef.color; - node.icon = fieldDef.icon; - node.fieldDef = fieldDef; - return true; - } - return false; - }); + // Internal utility function for calling the Graph API and handling the response + // by merging results into existing nodes in this workspace. + this.callElasticsearch = function(request) { + self.lastRequest = JSON.stringify(request, null, '\t'); + graphExplorer(self.options.indexName, request, function(data) { + self.lastResponse = JSON.stringify(data, null, '\t'); + const edges = []; + //Label the nodes with field number for CSS styling + data.vertices.forEach(node => { + self.options.vertex_fields.some(fieldDef => { + if (node.field === fieldDef.name) { + node.color = fieldDef.color; + node.icon = fieldDef.icon; + node.fieldDef = fieldDef; + return true; + } + return false; }); + }); - //Size the edges depending on weight - const minLineSize = 2; - const maxLineSize = 10; - let maxEdgeWeight = 0.00000001; - data.connections.forEach(edge => { - maxEdgeWeight = Math.max(maxEdgeWeight, edge.weight); - }); - data.connections.forEach(edge => { - edges.push({ - source: edge.source, - target: edge.target, - doc_count: edge.doc_count, - weight: edge.weight, - width: Math.max(minLineSize, (edge.weight / maxEdgeWeight) * maxLineSize), - }); + //Size the edges depending on weight + const minLineSize = 2; + const maxLineSize = 10; + let maxEdgeWeight = 0.00000001; + data.connections.forEach(edge => { + maxEdgeWeight = Math.max(maxEdgeWeight, edge.weight); + }); + data.connections.forEach(edge => { + edges.push({ + source: edge.source, + target: edge.target, + doc_count: edge.doc_count, + weight: edge.weight, + width: Math.max(minLineSize, (edge.weight / maxEdgeWeight) * maxLineSize), }); - - self.mergeGraph( - { - nodes: data.vertices, - edges: edges, - }, - { - labeller: self.options.labeller, - } - ); }); - }; - } - //===================== - // Begin Kibana wrapper - return { - createWorkspace: createWorkspace, + self.mergeGraph( + { + nodes: data.vertices, + edges: edges, + }, + { + labeller: self.options.labeller, + } + ); + }); }; -})(); +} +//===================== + +// Begin Kibana wrapper +export function createWorkspace(options) { + return new GraphWorkspace(options); +} diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js b/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js index 6f81a443086c05..7ffb16d986a21c 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js +++ b/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import gws from './graph_client_workspace'; +import { createWorkspace } from './graph_client_workspace'; describe('graphui-workspace', function() { describe('createWorkspace()', function() { @@ -38,7 +38,7 @@ describe('graphui-workspace', function() { minDocCount: 1, }, }; - const workspace = gws.createWorkspace(options); + const workspace = createWorkspace(options); return { workspace, //, get to(){} diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 53175d18e629f9..7effe44375b1f7 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -23,7 +23,7 @@ import { Listing } from './components/listing'; import { Settings } from './components/settings'; import { GraphVisualization } from './components/graph_visualization'; -import gws from './angular/graph_client_workspace.js'; +import { createWorkspace } from './angular/graph_client_workspace.js'; import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs } from './services/url'; import { createCachedIndexPatternProvider } from './services/index_pattern_cache'; import { urlTemplateRegex } from './helpers/url_template'; @@ -277,7 +277,7 @@ export function initGraphApp(angularModule, deps) { searchProxy: callSearchNodeProxy, exploreControls, }; - $scope.workspace = gws.createWorkspace(options); + $scope.workspace = createWorkspace(options); }, setLiveResponseFields: fields => { $scope.liveResponseFields = fields; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js index b8a14650d3fd5b..d24577826838e6 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -const icalendar = require('icalendar'); +import icalendar from 'icalendar'; import moment from 'moment'; import { generateTempId } from '../utils';