diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c51158a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +node_modules +/build +/local +/target diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..ddf9f4b --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,29 @@ +env: + browser: true + es6: true +extends: 'eslint:recommended' +parserOptions: + ecmaVersion: 2018 + sourceType: module +rules: + "prettier/prettier": off + indent: + - error + - 2 + linebreak-style: + - error + - unix + quotes: + - error + - single + semi: + - error + - always + no-trailing-spaces: + - 2 + - skipBlankLines: false + no-console: error +overrides: + - files: ['index.js'] + env: + node: true diff --git a/.gitignore b/.gitignore index 4833180..1ce9a64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ -node_modules/* -.nyc_output +node_modules/ +build/ +target/ +local +npm-debug.log* +package-lock.json +yarn.lock +.project +*.zip diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index e0ac14f..0000000 --- a/.jshintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "esversion" : 6 -} diff --git a/index.js b/index.js deleted file mode 100644 index 7c51e5f..0000000 --- a/index.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = function (kibana) { - return new kibana.Plugin({ - name: 'kbn_sankey_vis', - require: ['kibana', 'elasticsearch'], - uiExports: { - visTypes: [ - 'plugins/kbn_sankey_vis/kbn_sankey_vis' - ] - } - }); -}; diff --git a/kibana.json b/kibana.json new file mode 100644 index 0000000..47639c6 --- /dev/null +++ b/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "kbnSankeyVis", + "version": "7.10.2", + "server": false, + "ui": true, + "requiredPlugins": [ + "visualizations", + "data", + "inspector", + "kibanaLegacy" + ], + "requiredBundles": [ + "kibanaUtils", + "visDefaultEditor" + ] +} diff --git a/package.json b/package.json index a4b271b..be24d28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { - "name": "kbn_sankey_vis", - "version": "7.6.3", + "name": "kbn-sankey-vis", + "version": "7.10.2", + "kibana": { + "version": "kibana" + }, "authors": [ "Chenryn ", "Ch-bas " @@ -11,14 +14,17 @@ "repository": "https://github.com/uniberg/kbn_sankey_vis.git", "main": "index.js", "scripts": { - "test": "nyc --all mocha" + "start": "cd ../.. && node scripts/kibana --dev --oss", + "test": "nyc --all mocha", + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers" }, "dependencies": { "d3-plugins-sankey": "https://github.com/uniberg/d3-plugins-sankey.git", "json-stable-stringify": "^1.0.1" }, "devDependencies": { - "@fortawesome/fontawesome-free": "^5.10.2", + "@fortawesome/fontawesome-free": "5.15.2", "mocha": "~5.0.4", "nyc": "^13.3.0" } diff --git a/public/components/sankey_options.tsx b/public/components/sankey_options.tsx new file mode 100644 index 0000000..3e7c4a8 --- /dev/null +++ b/public/components/sankey_options.tsx @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +// For the current version of the Sankey Diagram, we do not need any option +// returning a react component is required +export const SankeyOptions = () => ( + <> +); diff --git a/public/enhanced-table-vis-params.scss b/public/enhanced-table-vis-params.scss new file mode 100644 index 0000000..a44ee09 --- /dev/null +++ b/public/enhanced-table-vis-params.scss @@ -0,0 +1,13 @@ +.enhanced-table-vis-params { + + .computed-columns .euiButtonIcon { + font-size: 0.8rem; + min-width: 12px; + padding: 0px; + } + + .ng-invalid.ng-touched .form-control, .ng-invalid.display-error .form-control { + border-color: #BD271E; + } +} + diff --git a/public/field_formatter.ts b/public/field_formatter.ts new file mode 100644 index 0000000..ae89e52 --- /dev/null +++ b/public/field_formatter.ts @@ -0,0 +1,11 @@ +import { getFormatService } from './services'; +import { IAggConfig, IFieldFormat, FieldFormatsContentType } from '../../../src/plugins/data/public'; + +/** + * Returns the fieldFormatter function associated to aggConfig, for the requested contentType (html or text). + * Returned fieldFormatter is a function, whose prototype is: fieldFormatter(value, options?) + */ +export function fieldFormatter(aggConfig: IAggConfig, contentType: FieldFormatsContentType) { + const fieldFormat: IFieldFormat = getFormatService().deserialize(aggConfig.toSerializedFieldFormat()); + return fieldFormat.getConverterFor(contentType); +} diff --git a/public/get_inner_angular.ts b/public/get_inner_angular.ts new file mode 100644 index 0000000..a713f08 --- /dev/null +++ b/public/get_inner_angular.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// inner angular imports +// these are necessary to bootstrap the local angular. +// They can stay even after NP cutover +import angular from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; +import 'angular-recursion'; +import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; +import { + initAngularBootstrap, + PaginateDirectiveProvider, + PaginateControlsDirectiveProvider, + PrivateProvider, + watchMultiDecorator, + KbnAccessibleClickProvider, +} from '../../../src/plugins/kibana_legacy/public'; + +initAngularBootstrap(); + +const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; + +export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { + const uiModule = getInnerAngular(name, core); + return uiModule; +} + +let initialized = false; + +export function getInnerAngular(name = 'kibana/kbn_sankey_vis', core: CoreStart) { + if (!initialized) { + createLocalPrivateModule(); + createLocalConfigModule(core.uiSettings); + initialized = true; + } + return angular + .module(name, [ + ...thirdPartyAngularDependencies, + 'tableVisConfig', + 'tableVisPrivate', + ]) + .config(watchMultiDecorator) + .directive('kbnAccessibleClick', KbnAccessibleClickProvider); +} + +function createLocalPrivateModule() { + angular.module('tableVisPrivate', []).provider('Private', PrivateProvider); +} + +function createLocalConfigModule(uiSettings: IUiSettingsClient) { + angular.module('tableVisConfig', []).provider('config', function () { + return { + $get: () => ({ + get: (value: string) => { + return uiSettings ? uiSettings.get(value) : undefined; + }, + // set method is used in agg_table mocha test + set: (key: string, value: string) => { + return uiSettings ? uiSettings.set(key, value) : undefined; + }, + }), + }; + }); +} diff --git a/public/index.scss b/public/index.scss new file mode 100644 index 0000000..0ed704d --- /dev/null +++ b/public/index.scss @@ -0,0 +1,19 @@ +visualization.enhanced-table { + padding: 0px 5px; +} + +.enhanced-table-vis { + $euiColorLightestShade: #F5F7FA; + @import 'agg_table/index'; + @import 'paginated_table/index'; + @import 'enhanced-table-vis'; +} + +.theme-dark.enhanced-table-vis { + $euiColorLightestShade: #25262E; + @import 'agg_table/index'; + @import 'paginated_table/index'; + @import 'enhanced-table-vis'; +} + +@import 'enhanced-table-vis-params'; diff --git a/public/index.ts b/public/index.ts new file mode 100644 index 0000000..3829fb8 --- /dev/null +++ b/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { PluginInitializerContext } from 'kibana/public'; +import { EnhancedTablePlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/public/kbn_sankey_vis.html b/public/kbn_sankey_vis.html index 217630a..24ebb5d 100644 --- a/public/kbn_sankey_vis.html +++ b/public/kbn_sankey_vis.html @@ -1,14 +1,34 @@ -
-
- -

No results found

+
+ +
+
+ + +
+
+ +

+

+
+
+ +
+ pppppp {{hasSomeRows}} + + +
-
diff --git a/public/kbn_sankey_vis.js b/public/kbn_sankey_vis.js index db061b1..2e3508a 100644 --- a/public/kbn_sankey_vis.js +++ b/public/kbn_sankey_vis.js @@ -1,55 +1,109 @@ - // Kibana Dependencies - import { npSetup } from 'ui/new_platform'; - import { AngularVisController } from 'ui/vis/vis_types/angular_vis_type'; - import { Schemas } from 'ui/vis/editors/default/schemas'; - import { setup as visualizations } from '../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ - // Own Dependencies - import KbnSankeyVisController from './kbn_sankey_vis_controller' - import random from '@fortawesome/fontawesome-free/svgs/solid/random.svg'; - import 'plugins/kbn_sankey_vis/kbn_sankey_vis.less'; - import kbnSankeyVisTemplate from 'plugins/kbn_sankey_vis/kbn_sankey_vis.html'; +import { i18n } from '@kbn/i18n'; +import { AggGroupNames } from '../../../src/plugins/data/public'; +import { Schemas } from '../../../src/plugins/vis_default_editor/public'; - const sankeyDef = { - name: 'kbn_sankey', - title: 'Sankey Diagram', - image: random, - description: 'A sankey diagram is a type of flow diagram where flow quantities are depicted by proportional arrow magnitutes.', - visualization: AngularVisController, - visConfig: { - defaults: { - showMetricsAtAllLevels: false +import sankeyTemplate from './kbn_sankey_vis.html'; +import { getSankeyVisualizationController } from './vis_controller'; +import { requestHandler } from './lib/request-handler'; +import { sankeyProvider } from './lib/agg_response'; +import { SankeyOptions } from './components/sankey_options'; +import { VIS_EVENT_TO_TRIGGER } from '../../../src/plugins/visualizations/public'; +import random from '@fortawesome/fontawesome-free/svgs/solid/random.svg'; + +// define the visType object, which kibana will use to display and configure new Vis object of this type. +export function sankeyTypeDefinition (core, context) { + return { + name: 'kbn_sankey', + title: i18n.translate('visTypeSankey.visTitle', { + defaultMessage: 'Sankey Diagram' + }), + image: random, + description: i18n.translate('visTypeSankey.visDescription', { + defaultMessage: 'A sankey diagram is a type of flow diagram where flow quantities are depicted by proportional arrow magnitutes.' + }), + visualization: getSankeyVisualizationController(core, context), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null }, - template: kbnSankeyVisTemplate - }, - hierarchicalData: function (vis) { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + showTotal: false, + totalFunc: 'sum', + computedColumns: [], + computedColsPerSplitCol: false, + hideExportLinks: false, + csvExportWithTotal: false, + stripedRows: false, + addRowNumberColumn: false, + csvEncoding: 'utf-8', + showFilterBar: false, + filterCaseSensitive: false, + filterBarHideable: false, + filterAsYouType: false, + filterTermsSeparately: false, + filterHighlightResults: false, + filterBarWidth: '25%' }, - editorConfig: { - optionsTemplate: '', - schemas: new Schemas([ - { - group: 'metrics', - name: 'metric', - title: 'Split Size', - min: 1, - max: 1, - defaults: [{ - type: 'count', - schema: 'metric' - }] + template: sankeyTemplate + }, + editorConfig: { + optionsTemplate: SankeyOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visType.VisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric' + }), + aggSettings: { + top_hits: { + allowStrings: true + } }, - { - group: 'buckets', - name: 'segment', - title: 'Split Slices', - aggFilter: '!geohash_grid', - min: 0, - max: Infinity - } - ]) - }, - requiresSearch: true - }; - npSetup.plugins.expressions.registerFunction(sankeyDef); - visualizations.types.createBaseVisualization(sankeyDef); + min: 1, + max: 1, + defaults: [{ type: 'count', schema: 'metric' }] + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visType.VisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows' + }), + min: 0, + }, + ]) + }, + requestHandler: requestHandler, + responseHandler: sankeyProvider, + hierarchicalData: (vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + } + }; +} diff --git a/public/kbn_sankey_vis_controller.js b/public/kbn_sankey_vis_controller.js index 31fb6ca..65773f9 100644 --- a/public/kbn_sankey_vis_controller.js +++ b/public/kbn_sankey_vis_controller.js @@ -1,19 +1,31 @@ -// Kibana Dependencies -import { uiModules } from 'ui/modules'; +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ // Own Dependencies import d3 from 'd3'; import 'd3-plugins-sankey'; -import AggResponseProvider from './lib/agg_response'; import { filterNodesAndLinks } from './lib/filter'; -import { isBackgroundDark, appropriateTextColor } from './lib/color_theme'; +// import { isBackgroundDark, appropriateTextColor } from './lib/color_theme'; -const module = uiModules.get('kibana/kbn_sankey_vis', ['kibana']); let observeResize = require('./lib/observe_resize'); -module.controller('KbnSankeyVisController', function ($scope, $element, $rootScope, Private) { - const sankeyAggResponse = Private(AggResponseProvider); - $scope.emptyGraph = false; +function KbnSankeyVisController ($scope, $element) { let svgRoot = $element[0]; let resize = false; @@ -24,7 +36,13 @@ module.controller('KbnSankeyVisController', function ($scope, $element, $rootSco let div; let svg; let globalData = null; + const uiStateSort = ($scope.uiState) ? $scope.uiState.get('vis.params.sort') : {}; + _.assign($scope.visParams.sort, uiStateSort); + $scope.sort = $scope.visParams.sort; + $scope.$watchCollection('sort', function (newSort) { + $scope.uiState.set('vis.params.sort', newSort); + }); let _updateDimensions = function _updateDimensions() { let delta = 10; let w = $element.parent().width() - 10; @@ -124,7 +142,7 @@ module.controller('KbnSankeyVisController', function ($scope, $element, $rootSco return d.color; }) .style('stroke', function (d) { - return (isBackgroundDark(null, null)) ? d3.rgb(d.color).brighter(2) : d3.rgb(d.color).darker(2); + return 1 === 1 ? d3.rgb(d.color).brighter(2) : d3.rgb(d.color).darker(2); }) .append('title') .text(function (d) { @@ -137,7 +155,6 @@ module.controller('KbnSankeyVisController', function ($scope, $element, $rootSco return d.dy / 2; }) .attr('dy', '.35em') - .style('fill', appropriateTextColor(null, null)) .attr('text-anchor', 'end') .attr('transform', null) .text(function (d) { @@ -189,22 +206,36 @@ module.controller('KbnSankeyVisController', function ($scope, $element, $rootSco d3.select(svgRoot).selectAll('svg').remove(); _buildVis(data); }; - let data; - $scope.$watch('esResponse', function (resp) { - if (resp) { - data = sankeyAggResponse($scope.vis, resp); - globalData = data; - if (data && data.slices){ - _updateDimensions(); - _render(data); + $scope.$watch('renderComplete', function () { + + if ($scope.esResponse && $scope.esResponse.newResponse) { + globalData = $scope.esResponse; + _updateDimensions(); + _render($scope.esResponse); + // init tableGroups + $scope.hasSomeRows = null; + $scope.hasSomeData = null; + $scope.tableGroups = null; + $scope.esResponse.newResponse = false; + const totalHits = $scope.esResponse.totalHits; + // no data to display + if (totalHits === 0) { + $scope.hasSomeRows = false; + $scope.hasSomeData = false; + $scope.renderComplete(); + return; } } + + $scope.renderComplete(); }); observeResize($element, function () { - if (data) { + if (globalData) { _updateDimensions(); resize=true; - _render(data); + _render(globalData); } }); -}); +} + +export { KbnSankeyVisController }; diff --git a/public/legacy.ts b/public/legacy.ts new file mode 100644 index 0000000..5a42fac --- /dev/null +++ b/public/legacy.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from '../../../src/core/public'; +import { npSetup, npStart } from 'ui/new_platform'; +import { plugin } from '.'; + +import { TablePluginSetupDependencies } from './plugin'; +import { TablePluginStartDependencies } from './plugin'; + +const plugins: Readonly = { + visualizations: npSetup.plugins.visualizations, +}; + +const startData: Readonly = { + data: npStart.plugins.data +} + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, plugins); +export const start = pluginInstance.start(npStart.core, startData); diff --git a/public/lib/agg_response.js b/public/lib/agg_response.js index 3b55bf5..1d5227b 100644 --- a/public/lib/agg_response.js +++ b/public/lib/agg_response.js @@ -1,46 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + const { aggregate } = require('./agg_response_helper'); import { bucketHelper } from './bucket_helper'; -import { toastNotifications } from 'ui/notify'; - -module.exports = function sankeyProvider() { - return function (vis, resp) { +export function sankeyProvider(resp) { + // When 'Show missing values' and/or 'Group bucket' is checked then + // group the inputs in different arrays + let missingValues = []; + let groupBucket = []; + resp.aggs.aggs.forEach((bucket) => { - // When 'Show missing values' and/or 'Group bucket' is checked then - // group the inputs in different arrays - let missingValues = []; - let groupBucket = []; - vis.aggs.aggs.forEach((bucket) => { + if (bucket.params.missingBucket) { + missingValues.push({[bucketHelper(resp, bucket, bucket.params.missingBucketLabel).id]: bucket.params.missingBucketLabel}); - if (bucket.params.missingBucket) { - missingValues = bucketHelper(resp, bucket, bucket.params.missingBucketLabel); - - } - if (bucket.params.otherBucket) { - groupBucket = bucketHelper(resp, bucket, bucket.params.otherBucketLabel); - } - }); + } + if (bucket.params.otherBucket) { + groupBucket.push({[bucketHelper(resp, bucket, bucket.params.otherBucket).id]: bucket.params.otherBucketLabel}); + } + }); - if (resp.rows.length > 1) { - if (resp.rows && resp.rows.length > 0) { - return { - slices: aggregate({ - rows: resp.rows, - missingValues, - groupBucket - }) - }; - } else { - toastNotifications.addDanger('Empty response.'); - return { - slices: { nodes: [], links: [] } - }; - } + if (resp.rows.length > 1) { + if (resp.rows && resp.rows.length > 0) { + return { + slices: aggregate({ + rows: resp.rows, + missingValues, + groupBucket + }), totalHits: resp.totalHits, aggs: resp.aggs, newResponse: true + }; } else { - toastNotifications.addDanger('Minimum two sub aggs needed.'); return { - slices: { nodes: [], links: [] } + slices: { nodes: [], links: [] }, totalHits: resp.totalHits, aggs: resp.aggs, newResponse: true }; } + } else { + return { + slices: { nodes: [], links: [] }, totalHits: resp.totalHits, aggs: resp.aggs, newResponse: true + }; } } diff --git a/public/lib/agg_response_helper.js b/public/lib/agg_response_helper.js index c4688d4..a2de88d 100644 --- a/public/lib/agg_response_helper.js +++ b/public/lib/agg_response_helper.js @@ -100,12 +100,12 @@ module.exports = (function() { // Update the bucket if 'Show missing values' is checked // by default, the value is '__missing__' // kibana/kibana-repo/src/ui/public/agg_types/buckets/terms.js - if (bucketCopy[cell] === '__missing__') { - bucketReplaceProperty(missingValues, bucketCopy); + if (bucketCopy[cell] === '__missing__' && missingValues.length > 0) { + bucketReplaceProperty(missingValues, bucketCopy, cell); } // Update the bucket if 'Group other bucket' is checked - if (bucketCopy[cell] === '__other__') { - bucketReplaceProperty(groupBucket, bucketCopy); + if (bucketCopy[cell] === '__other__' && groupBucket.length > 0) { + bucketReplaceProperty(groupBucket, bucketCopy, cell); } Object.defineProperty( bucketCopy, diff --git a/public/lib/bucket_helper.js b/public/lib/bucket_helper.js index 7c42146..15d6f22 100644 --- a/public/lib/bucket_helper.js +++ b/public/lib/bucket_helper.js @@ -9,12 +9,6 @@ * ... * ] */ -export const bucketHelper = (response, bucket, label) => { - let tmpArr = []; - response.columns.find( column => { - if ((column.name.search(bucket.params.field.name) !== -1)) { - tmpArr.push({[column.id]: label}); - } - }) - return tmpArr; -} +export const bucketHelper = (response, bucket) => { + return(response.columns.find( column => column.name.search(bucket.params.field.displayName) !== -1)); +}; diff --git a/public/lib/bucket_replace_property_helper.js b/public/lib/bucket_replace_property_helper.js index 1ab492b..70b633d 100644 --- a/public/lib/bucket_replace_property_helper.js +++ b/public/lib/bucket_replace_property_helper.js @@ -13,14 +13,10 @@ * ... * } */ -export const bucketReplaceProperty = (sourceBucket, destinationBucket) => { - for(let bucketArray of sourceBucket) { - Object.getOwnPropertyNames(bucketArray).forEach( (cell) => { - if (destinationBucket.hasOwnProperty(cell)) { - destinationBucket[cell] = bucketArray[cell]; - } - } - ); - } +export const bucketReplaceProperty = (sourceBucket, destinationBucket, cell) => { + // userDefinedArrayLabels stores the defined 'other' or 'missing' values defined by the user when he checks + // 'Group other values' or 'Show missing values' + const userDefinedArrayLabels = sourceBucket.find(element => element.hasOwnProperty(cell)); + destinationBucket[cell] = userDefinedArrayLabels[cell]; return destinationBucket; -} +}; diff --git a/public/lib/color_theme.js b/public/lib/color_theme.js deleted file mode 100644 index 49ebee1..0000000 --- a/public/lib/color_theme.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Adapted from Kibana's set_is_reversed.js - * (originally at https://github.com/elastic/kibana/blob/bf04235/ - * src/plugins/vis_type_timeseries/public/application/lib/set_is_reversed.js) - * for use in kbn_sankey_vis plugin. - * - * Kibana's original license terms for set_is_reversed.js are as follows: - * - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import color from 'color'; -import { getUISettings } from '../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/services'; - -const isDarkTheme = () => getUISettings().get('theme:darkMode'); -const lightTextColor = "#CBCFCB"; -const darkTextColor = "#000000"; - -/** - * Returns true if the color that is passed has low luminosity - */ -const isColorDark = (c) => { - return color(c).luminosity() < 0.45; -}; - -/** - * Checks to see if the `currentTheme` is dark in luminosity. - * Defaults to checking `theme:darkMode`. - */ -export const isThemeDark = (currentTheme) => { - let themeIsDark = currentTheme || isDarkTheme(); - - // If passing a string, check the luminosity - if (typeof currentTheme === 'string') { - themeIsDark = isColorDark(currentTheme); - } - - return themeIsDark; -}; - -/** - * Checks to find if the ultimate `backgroundColor` is dark. - * Defaults to returning if the `currentTheme` is dark. - */ -export const isBackgroundDark = (backgroundColor, currentTheme) => { - const themeIsDark = isThemeDark(currentTheme); - - // If a background color doesn't exist or it inherits, pass back if it's a darktheme - if (!backgroundColor || backgroundColor === 'inherit') { - return themeIsDark; - } - - // Otherwise return if the background color has low luminosity - return isColorDark(backgroundColor); -}; - -/** - * Checks to see if `backgroundColor` is the the same lightness spectrum as `currentTheme`. - */ -export const isBackgroundInverted = (backgroundColor, currentTheme) => { - const backgroundIsDark = isBackgroundDark(backgroundColor, currentTheme); - const themeIsDark = isThemeDark(currentTheme); - return backgroundIsDark !== themeIsDark; -}; - -/** - * Return an appropriate text color for the given `backgroundColor` and `currentTheme` - */ -export const appropriateTextColor = (backgroundColor, currentTheme) => { - return (isBackgroundDark(backgroundColor, currentTheme)) ? lightTextColor : darkTextColor; -}; diff --git a/public/lib/kibana_cloned_code/courier.ts b/public/lib/kibana_cloned_code/courier.ts new file mode 100644 index 0000000..388aa33 --- /dev/null +++ b/public/lib/kibana_cloned_code/courier.ts @@ -0,0 +1,197 @@ +import { hasIn } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { calculateObjectHash } from '../../../../../src/plugins/kibana_utils/public'; +import { PersistedState } from '../../../../../src/plugins/visualizations/public'; +import { Adapters } from '../../../../../src/plugins/inspector/public'; + +import { IAggConfigs } from '../../../../../src/plugins/data/public/search/aggs'; +import { ISearchSource } from '../../../../../src/plugins/data/public/search/search_source'; +import { + calculateBounds, + Filter, + getTime, + IIndexPattern, + isRangeFilter, + Query, + TimeRange, +} from '../../../../../src/plugins/data/common'; +import { FilterManager } from '../../../../../src/plugins/data/public/query'; +import { buildTabularInspectorData } from './build_tabular_inspector_data'; +import { search } from '../../../../../src/plugins/data/public'; + +import { getFormatService as getFieldFormats } from '../../services'; + +/** + * Clone of: ../../../../../src/plugins/data/public/search/expressions/esaggs.ts + * Components: RequestHandlerParams and handleCourierRequest + */ +interface RequestHandlerParams { + searchSource: ISearchSource; + aggs: IAggConfigs; + timeRange?: TimeRange; + timeFields?: string[]; + indexPattern?: IIndexPattern; + query?: Query; + filters?: Filter[]; + forceFetch: boolean; + filterManager: FilterManager; + uiState?: PersistedState; + partialRows?: boolean; + inspectorAdapters: Adapters; + metricsAtAllLevels?: boolean; + visParams?: any; + abortSignal?: AbortSignal; +} + +export const handleCourierRequest = async ({ + searchSource, + aggs, + timeRange, + timeFields, + indexPattern, + query, + filters, + forceFetch, + partialRows, + metricsAtAllLevels, + inspectorAdapters, + filterManager, + abortSignal, +}: RequestHandlerParams) => { + // Create a new search source that inherits the original search source + // but has the appropriate timeRange applied via a filter. + // This is a temporary solution until we properly pass down all required + // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641). + // Using callParentStartHandlers: true we make sure, that the parent searchSource + // onSearchRequestStart will be called properly even though we use an inherited + // search source. + const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); + const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); + + aggs.setTimeRange(timeRange as TimeRange); + + // For now we need to mirror the history of the passed search source, since + // the request inspector wouldn't work otherwise. + Object.defineProperty(requestSearchSource, 'history', { + get() { + return searchSource.history; + }, + set(history) { + return (searchSource.history = history); + }, + }); + + requestSearchSource.setField('aggs', function () { + return aggs.toDsl(metricsAtAllLevels); + }); + + requestSearchSource.onRequestStart((paramSearchSource, options) => { + return aggs.onSearchRequestStart(paramSearchSource, options); + }); + + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (timeRange && allTimeFields.length > 0) { + timeFilterSearchSource.setField('filter', () => { + return allTimeFields + .map((fieldName) => getTime(indexPattern, timeRange, { fieldName })) + .filter(isRangeFilter); + }); + } + + requestSearchSource.setField('filter', filters); + requestSearchSource.setField('query', query); + + const reqBody = await requestSearchSource.getSearchRequestBody(); + + const queryHash = calculateObjectHash(reqBody); + // We only need to reexecute the query, if forceFetch was true or the hash of the request body has changed + // since the last request + const shouldQuery = forceFetch || (searchSource as any).lastQuery !== queryHash; + + if (shouldQuery) { + inspectorAdapters.requests.reset(); + const request = inspectorAdapters.requests.start( + i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + } + ); + request.stats(search.getRequestInspectorStats(requestSearchSource)); + + try { + const response = await requestSearchSource.fetch({ abortSignal }); + + (searchSource as any).lastQuery = queryHash; + + request.stats(search.getResponseInspectorStats(searchSource, response)).ok({ json: response }); + + (searchSource as any).rawResponse = response; + } catch (e) { + // Log any error during request to the inspector + request.error({ json: e }); + throw e; + } finally { + // Add the request body no matter if things went fine or not + requestSearchSource.getSearchRequestBody().then((req: unknown) => { + request.json(req); + }); + } + } + + // Note that rawResponse is not deeply cloned here, so downstream applications using courier + // must take care not to mutate it, or it could have unintended side effects, e.g. displaying + // response data incorrectly in the inspector. + let resp = (searchSource as any).rawResponse; + for (const agg of aggs.aggs) { + if (hasIn(agg, 'type.postFlightRequest')) { + resp = await agg.type.postFlightRequest( + resp, + aggs, + agg, + requestSearchSource, + inspectorAdapters.requests, + abortSignal + ); + } + } + + (searchSource as any).finalResponse = resp; + + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; + const tabifyParams = { + metricsAtAllLevels, + partialRows, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, + }; + + const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams }); + // We only need to reexecute tabify, if either we did a new request or some input params to tabify changed + const shouldCalculateNewTabify = + shouldQuery || (searchSource as any).lastTabifyHash !== tabifyCacheHash; + + if (shouldCalculateNewTabify) { + (searchSource as any).lastTabifyHash = tabifyCacheHash; + (searchSource as any).tabifiedResponse = search.tabifyAggResponse( + aggs, + (searchSource as any).finalResponse, + tabifyParams + ); + } + + return (searchSource as any).tabifiedResponse; +}; diff --git a/public/lib/kibana_cloned_code/utils.ts b/public/lib/kibana_cloned_code/utils.ts new file mode 100644 index 0000000..8fa335f --- /dev/null +++ b/public/lib/kibana_cloned_code/utils.ts @@ -0,0 +1,19 @@ +import { IFieldFormat } from '../../../../../src/plugins/data/common'; + +/** + * Clone of: '../../../../../src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts' + * Component: serializeAggConfig + */ +export function serializeAggConfig(aggConfig) { + return { + type: aggConfig.type.name, + indexPatternId: aggConfig.getIndexPattern().id, + aggConfigParams: aggConfig.serialize().params, + }; +}; + +/** + * Clone of: '../../../../../src/plugins/data/common/field_formats/utils.ts' + * Component: FormatFactory +*/ +export type FormatFactory = (mapping?) => IFieldFormat; diff --git a/public/lib/request-handler.js b/public/lib/request-handler.js new file mode 100644 index 0000000..8e065f5 --- /dev/null +++ b/public/lib/request-handler.js @@ -0,0 +1,149 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { RequestAdapter, DataAdapter } from '../../../../src/plugins/inspector/public'; +import { getSearchService, getQueryService } from '../services'; +import { handleCourierRequest } from './kibana_cloned_code/courier'; +import { serializeAggConfig } from './kibana_cloned_code/utils'; + +export async function requestHandler ({ + partialRows, + metricsAtAllLevels, + visParams, + timeRange, + query, + filters, + inspectorAdapters, + forceFetch, + aggs +}) { + + const { filterManager } = getQueryService(); + const MAX_HITS_SIZE = 10000; + + // create search source with query parameters + const searchService = getSearchService(); + const searchSource = await searchService.searchSource.create(); + searchSource.setField('index', aggs.indexPattern); + let hitsSize = (visParams.hitsSize !== undefined ? Math.min(visParams.hitsSize, MAX_HITS_SIZE) : 0); + searchSource.setField('size', hitsSize); + + // specific request params for "field columns" + if (visParams.fieldColumns !== undefined) { + if (!visParams.fieldColumns.some(fieldColumn => fieldColumn.field.name === '_source')) { + searchSource.setField('_source', visParams.fieldColumns.map(fieldColumn => fieldColumn.field.name)); + } + searchSource.setField('docvalue_fields', visParams.fieldColumns.filter(fieldColumn => fieldColumn.field.readFromDocValues).map(fieldColumn => fieldColumn.field.name)); + const scriptFields = {}; + visParams.fieldColumns.filter(fieldColumn => fieldColumn.field.scripted).forEach(fieldColumn => { + scriptFields[fieldColumn.field.name] = { + script: { + source: fieldColumn.field.script + } + }; + }); + searchSource.setField('script_fields', scriptFields); + } + + // set search sort + if (visParams.sortField !== undefined) { + searchSource.setField('sort', [{ + [visParams.sortField.name]: { + order: visParams.sortOrder + } + }]); + if ((visParams.hitsSize !== undefined && visParams.hitsSize > MAX_HITS_SIZE) || visParams.csvFullExport) { + searchSource.getField('sort').push({'_id': {'order': 'asc','unmapped_type': 'keyword'}}); + } + } + + // add 'count' metric if there is no input column + if (aggs.aggs.length === 0) { + aggs.createAggConfig({ + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: {} + }); + } + + inspectorAdapters.requests = new RequestAdapter(); + inspectorAdapters.data = new DataAdapter(); + + // execute elasticsearch query + const request = { + searchSource, + aggs, + indexPattern: aggs.indexPattern, + timeRange, + query, + filters, + forceFetch, + metricsAtAllLevels, + partialRows, + inspectorAdapters, + filterManager + }; + const response = await handleCourierRequest(request); + + // set 'split tables' direction + const splitAggs = aggs.bySchemaName('split'); + if (splitAggs.length > 0) { + splitAggs[0].params.row = visParams.row; + } + + // enrich response: total & aggs + response.totalHits = _.get(searchSource, 'finalResponse.hits.total', -1); + response.aggs = aggs; + response.columns.forEach(column => { + column.meta = serializeAggConfig(column.aggConfig); + }); + + // enrich response: hits + if (visParams.fieldColumns !== undefined) { + response.fieldColumns = visParams.fieldColumns; + response.hits = _.get(searchSource, 'finalResponse.hits.hits', []); + + // continue requests until expected hits size is reached + if (visParams.hitsSize !== undefined && visParams.hitsSize > MAX_HITS_SIZE && response.totalHits > MAX_HITS_SIZE) { + let remainingSize = visParams.hitsSize; + do { + remainingSize -= hitsSize; + const searchAfter = response.hits[response.hits.length - 1].sort; + hitsSize = Math.min(remainingSize, MAX_HITS_SIZE); + searchSource.setField('size', hitsSize); + searchSource.setField('search_after', searchAfter); + await handleCourierRequest(request); + const nextResponseHits = _.get(searchSource, 'finalResponse.hits.hits', []); + for (let i = 0; i < nextResponseHits.length; i++) { + response.hits.push(nextResponseHits[i]); + } + } while (remainingSize > hitsSize); + } + + // put request on response, if full csv download is enabled + if (visParams.csvFullExport) { + response.request = request; + } + } + // return elasticsearch response + return response; +} diff --git a/public/plugin.ts b/public/plugin.ts new file mode 100644 index 0000000..05d1d36 --- /dev/null +++ b/public/plugin.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; + +import { sankeyTypeDefinition } from './kbn_sankey_vis'; + +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { setNotifications, setQueryService, setSearchService } from './services'; +import { KibanaLegacyStart } from '../../../src/plugins/kibana_legacy/public'; + + +/** @internal */ +export interface TablePluginSetupDependencies { + visualizations: VisualizationsSetup; +} + +/** @internal */ +export interface TablePluginStartDependencies { + data: DataPublicPluginStart; + kibanaLegacy: KibanaLegacyStart; +} + +/** @internal */ +export class EnhancedTablePlugin implements Plugin, void> { + initializerContext: PluginInitializerContext; + createBaseVisualization: any; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public async setup( + core: CoreSetup, + { visualizations }: TablePluginSetupDependencies + ) { + visualizations.createBaseVisualization( + sankeyTypeDefinition(core, this.initializerContext) + ); + } + + public start(core: CoreStart, { data, kibanaLegacy }: TablePluginStartDependencies) { + setNotifications(core.notifications); + setQueryService(data.query); + setSearchService(data.search); + } +} diff --git a/public/services.ts b/public/services.ts new file mode 100644 index 0000000..deee6e2 --- /dev/null +++ b/public/services.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createGetterSetter } from '../../../src/plugins/kibana_utils/public'; +import { NotificationsStart } from '../../../src/core/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; + +export const [getNotifications, setNotifications] = createGetterSetter< + NotificationsStart +>('Notifications'); + +export const [getQueryService, setQueryService] = createGetterSetter< + DataPublicPluginStart['query'] +>('Query'); + +export const [getSearchService, setSearchService] = createGetterSetter< + DataPublicPluginStart['search'] +>('Search'); diff --git a/public/vis_controller.ts b/public/vis_controller.ts new file mode 100644 index 0000000..ac927d0 --- /dev/null +++ b/public/vis_controller.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; +import $ from 'jquery'; + +import { VisParams, ExprVis } from '../../../src/plugins/visualizations/public'; +import { getAngularModule } from './get_inner_angular'; +import { getKibanaLegacy } from './services'; +import { initVisLegacyModule } from './vis_legacy_module'; + +const innerAngularName = 'kibana/kbn_sankey_vis'; + +export function getSankeyVisualizationController( + core: CoreSetup, + context: PluginInitializerContext +) { + return class EnhancedTableVisualizationController { + private tableVisModule: IModule | undefined; + private injector: auto.IInjectorService | undefined; + el: JQuery; + vis: ExprVis; + $rootScope: IRootScopeService | null = null; + $scope: (IScope & { [key: string]: any }) | undefined; + $compile: ICompileService | undefined; + + constructor(domeElement: Element, vis: ExprVis) { + this.el = $(domeElement); + this.vis = vis; + } + + getInjector() { + if (!this.injector) { + const mountpoint = document.createElement('div'); + mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); + this.injector = angular.bootstrap(mountpoint, [innerAngularName]); + this.el.append(mountpoint); + } + + return this.injector; + } + + async initLocalAngular() { + if (!this.tableVisModule) { + const [coreStart] = await core.getStartServices(); + this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); + initVisLegacyModule(this.tableVisModule); + } + } + + async render(esResponse: object, visParams: VisParams) { + await this.initLocalAngular(); + + return new Promise(async (resolve, reject) => { + if (!this.$rootScope) { + const $injector = this.getInjector(); + this.$rootScope = $injector.get('$rootScope'); + this.$compile = $injector.get('$compile'); + } + const updateScope = () => { + if (!this.$scope) { + return; + } + + // How things get into this $scope? + // To inject variables into this $scope there's the following pipeline of stuff to check: + // - visualize_embeddable => that's what the editor creates to wrap this Angular component + // - build_pipeline => it serialize all the params into an Angular template compiled on the fly + // - table_vis_fn => unserialize the params and prepare them for the final React/Angular bridge + // - visualization_renderer => creates the wrapper component for this controller and passes the params + // + // In case some prop is missing check into the top of the chain if they are available and check + // the list above that it is passing through + this.$scope.vis = this.vis; + this.$scope.visState = { params: visParams, title: visParams.title }; + this.$scope.esResponse = esResponse; + + this.$scope.visParams = visParams; + this.$scope.renderComplete = resolve; + this.$scope.renderFailed = reject; + this.$scope.resize = Date.now(); + this.$scope.$apply(); + }; + + if (!this.$scope && this.$compile) { + this.$scope = this.$rootScope.$new(); + this.$scope.uiState = this.vis.getUiState(); + updateScope(); + this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); + this.$scope.$apply(); + } else { + updateScope(); + } + }); + } + + destroy() { + if (this.$rootScope) { + this.$rootScope.$destroy(); + this.$rootScope = null; + } + } + }; +} diff --git a/public/vis_legacy_module.ts b/public/vis_legacy_module.ts new file mode 100644 index 0000000..062b0ce --- /dev/null +++ b/public/vis_legacy_module.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IModule } from 'angular'; + +// @ts-ignore +import { KbnSankeyVisController } from './kbn_sankey_vis_controller'; + +/** @internal */ +export const initVisLegacyModule = (angularIns: IModule): void => { + angularIns + .controller('KbnSankeyVisController', KbnSankeyVisController); +};