[7.x] [Vis] TableVis uses local angular (#50759) (#51962)

* [Vis] TableVis uses local angular (#50759)

* TableVis uses local angular

* Clean up

* Fix TS

* Update angular_config

* Fix export

* Update render_app.ts

* Cetralize ui deps

* Fix loading KbnTableVisController in Dashboard

* Fix graph

* Rename const

* Add table vis mocks

* Fix kbn_top_nav

* Add TS for test

* Complete conversion paginated_table test to Jest

* Convert table_vis_controller test to Jest

* Convert table_vis_controller test to Jest

* Create agg_table.test.ts

* Fix mocha tests

* Refactoring

* Remove module dep

* Remove LegacyDependenciesPlugin

* Move file

* Fix path

* Fix path

* Fix TS

* Fix Jest test

* Fix TS

* Revert "Fix TS"

This reverts commit 4cff6f6767.
This commit is contained in:
Maryia Lapata 2019-12-03 12:32:59 +03:00 committed by GitHub
parent 542b94a242
commit 83fb4f6453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1486 additions and 879 deletions

View file

@ -1,193 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy';
import { Vis } from '../../../visualizations/public';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { AppStateProvider } from 'ui/state_management/app_state';
import { tabifyAggResponse } from 'ui/agg_response/tabify';
import { tableVisTypeDefinition } from '../table_vis_type';
import { setup as visualizationsSetup } from '../../../visualizations/public/np_ready/public/legacy';
describe('Table Vis - Controller', async function () {
let $rootScope;
let $compile;
let Private;
let $scope;
let $el;
let fixtures;
let AppState;
let tableAggResponse;
let tabifiedResponse;
ngMock.inject(function () {
visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition);
});
beforeEach(ngMock.module('kibana', 'kibana/table_vis'));
beforeEach(
ngMock.inject(function ($injector) {
Private = $injector.get('Private');
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
fixtures = require('fixtures/fake_hierarchical_data');
AppState = Private(AppStateProvider);
tableAggResponse = legacyResponseHandlerProvider().handler;
})
);
function OneRangeVis(params) {
return new Vis(Private(FixturesStubbedLogstashIndexPatternProvider), {
type: 'table',
params: params || {},
aggs: [
{ type: 'count', schema: 'metric' },
{
type: 'range',
schema: 'bucket',
params: {
field: 'bytes',
ranges: [{ from: 0, to: 1000 }, { from: 1000, to: 2000 }],
},
},
],
});
}
const dimensions = {
buckets: [
{
accessor: 0,
},
],
metrics: [
{
accessor: 1,
format: { id: 'range' },
},
],
};
// basically a parameterized beforeEach
function initController(vis) {
vis.aggs.aggs.forEach(function (agg, i) {
agg.id = 'agg_' + (i + 1);
});
tabifiedResponse = tabifyAggResponse(vis.aggs, fixtures.oneRangeBucket);
$rootScope.vis = vis;
$rootScope.visParams = vis.params;
$rootScope.uiState = new AppState({ uiState: {} }).makeStateful('uiState');
$rootScope.renderComplete = () => {};
$rootScope.newScope = function (scope) {
$scope = scope;
};
$el = $('<div>')
.attr('ng-controller', 'KbnTableVisController')
.attr('ng-init', 'newScope(this)');
$compile($el)($rootScope);
}
// put a response into the controller
function attachEsResponseToScope(resp) {
$rootScope.esResponse = resp;
$rootScope.$apply();
}
// remove the response from the controller
function removeEsResponseFromScope() {
delete $rootScope.esResponse;
$rootScope.renderComplete = () => {};
$rootScope.$apply();
}
it('exposes #tableGroups and #hasSomeRows when a response is attached to scope', async function () {
const vis = new OneRangeVis();
initController(vis);
expect(!$scope.tableGroups).to.be.ok();
expect(!$scope.hasSomeRows).to.be.ok();
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
expect($scope.hasSomeRows).to.be(true);
expect($scope.tableGroups).to.have.property('tables');
expect($scope.tableGroups.tables).to.have.length(1);
expect($scope.tableGroups.tables[0].columns).to.have.length(2);
expect($scope.tableGroups.tables[0].rows).to.have.length(2);
});
it('clears #tableGroups and #hasSomeRows when the response is removed', async function () {
const vis = new OneRangeVis();
initController(vis);
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
removeEsResponseFromScope();
expect(!$scope.hasSomeRows).to.be.ok();
expect(!$scope.tableGroups).to.be.ok();
});
it('sets the sort on the scope when it is passed as a vis param', async function () {
const sortObj = {
columnIndex: 1,
direction: 'asc',
};
const vis = new OneRangeVis({ sort: sortObj });
initController(vis);
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
expect($scope.sort.columnIndex).to.equal(sortObj.columnIndex);
expect($scope.sort.direction).to.equal(sortObj.direction);
});
it('sets #hasSomeRows properly if the table group is empty', async function () {
const vis = new OneRangeVis();
initController(vis);
tabifiedResponse.rows = [];
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
expect($scope.hasSomeRows).to.be(false);
expect(!$scope.tableGroups).to.be.ok();
});
it('passes partialRows:true to tabify based on the vis params', function () {
const vis = new OneRangeVis({ showPartialRows: true });
initController(vis);
expect(vis.isHierarchical()).to.equal(true);
});
it('passes partialRows:false to tabify based on the vis params', function () {
const vis = new OneRangeVis({ showPartialRows: false });
initController(vis);
expect(vis.isHierarchical()).to.equal(false);
});
});

View file

@ -23,14 +23,15 @@ import ngMock from 'ng_mock';
import expect from '@kbn/expect';
import fixtures from 'fixtures/fake_hierarchical_data';
import sinon from 'sinon';
import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy';
import { legacyResponseHandlerProvider, tabifyAggResponse, npStart } from '../../legacy_imports';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { Vis } from '../../../../visualizations/public';
import { tabifyAggResponse } from 'ui/agg_response/tabify';
import { round } from 'lodash';
import { Vis } from '../../../../visualizations/public';
import { tableVisTypeDefinition } from '../../table_vis_type';
import { setup as visualizationsSetup } from '../../../../visualizations/public/np_ready/public/legacy';
import { getAngularModule } from '../../get_inner_angular';
import { initTableVisLegacyModule } from '../../table_vis_legacy_module';
describe('Table Vis - AggTable Directive', function () {
let $rootScope;
@ -96,11 +97,18 @@ describe('Table Vis - AggTable Directive', function () {
);
};
const initLocalAngular = () => {
const tableVisModule = getAngularModule('kibana/table_vis', npStart.core);
initTableVisLegacyModule(tableVisModule);
};
beforeEach(initLocalAngular);
ngMock.inject(function () {
visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition);
});
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.module('kibana/table_vis'));
beforeEach(
ngMock.inject(function ($injector, Private, config) {
tableAggResponse = legacyResponseHandlerProvider().handler;

View file

@ -21,10 +21,11 @@ import $ from 'jquery';
import ngMock from 'ng_mock';
import expect from '@kbn/expect';
import fixtures from 'fixtures/fake_hierarchical_data';
import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy';
import { legacyResponseHandlerProvider, tabifyAggResponse, npStart } from '../../legacy_imports';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { Vis } from 'ui/vis';
import { tabifyAggResponse } from 'ui/agg_response/tabify';
import { Vis } from '../../../../visualizations/public';
import { getAngularModule } from '../../get_inner_angular';
import { initTableVisLegacyModule } from '../../table_vis_legacy_module';
describe('Table Vis - AggTableGroup Directive', function () {
let $rootScope;
@ -52,7 +53,14 @@ describe('Table Vis - AggTableGroup Directive', function () {
tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets);
};
beforeEach(ngMock.module('kibana'));
const initLocalAngular = () => {
const tableVisModule = getAngularModule('kibana/table_vis', npStart.core);
initTableVisLegacyModule(tableVisModule);
};
beforeEach(initLocalAngular);
beforeEach(ngMock.module('kibana/table_vis'));
beforeEach(
ngMock.inject(function ($injector, Private) {
// this is provided in table_vis_controller.js

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import aggTableTemplate from './agg_table.html';
import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
import { getFormat } from '../legacy_imports';
import { i18n } from '@kbn/i18n';
export function KbnAggTable(config, RecursionHelper) {

View file

@ -23,8 +23,7 @@ import { EuiIconTip, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns';
import { VisOptionsProps } from 'ui/vis/editors/default';
import { tabifyGetColumns, VisOptionsProps } from '../legacy_imports';
import {
NumberInputOption,
SwitchOption,

View file

@ -0,0 +1,104 @@
/*
* 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';
import 'ui/angular-bootstrap';
import 'angular-recursion';
import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular';
import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public';
import {
PrivateProvider,
PaginateDirectiveProvider,
PaginateControlsDirectiveProvider,
watchMultiDecorator,
KbnAccessibleClickProvider,
StateManagementConfigProvider,
configureAppAngularModule,
} from './legacy_imports';
const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper'];
export function getAngularModule(name: string, core: CoreStart) {
const uiModule = getInnerAngular(name, core);
configureAppAngularModule(uiModule, core as LegacyCoreStart, true);
return uiModule;
}
let initialized = false;
export function getInnerAngular(name = 'kibana/table_vis', core: CoreStart) {
if (!initialized) {
createLocalPrivateModule();
createLocalI18nModule();
createLocalConfigModule(core.uiSettings);
createLocalPaginateModule();
initialized = true;
}
return angular
.module(name, [
...thirdPartyAngularDependencies,
'tableVisPaginate',
'tableVisConfig',
'tableVisPrivate',
'tableVisI18n',
])
.config(watchMultiDecorator)
.directive('kbnAccessibleClick', KbnAccessibleClickProvider);
}
function createLocalPrivateModule() {
angular.module('tableVisPrivate', []).provider('Private', PrivateProvider);
}
function createLocalConfigModule(uiSettings: IUiSettingsClient) {
angular
.module('tableVisConfig', ['tableVisPrivate'])
.provider('stateManagementConfig', StateManagementConfigProvider)
.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;
},
}),
};
});
}
function createLocalI18nModule() {
angular
.module('tableVisI18n', [])
.provider('i18n', I18nProvider)
.filter('i18n', i18nFilter)
.directive('i18nId', i18nDirective);
}
function createLocalPaginateModule() {
angular
.module('tableVisPaginate', [])
.directive('paginate', PaginateDirectiveProvider)
.directive('paginateControls', PaginateControlsDirectiveProvider);
}

View file

@ -18,20 +18,15 @@
*/
import { PluginInitializerContext } from 'kibana/public';
import { npSetup, npStart } from 'ui/new_platform';
import { npSetup, npStart } from './legacy_imports';
import { plugin } from '.';
import { TablePluginSetupDependencies } from './plugin';
import { setup as visualizationsSetup } from '../../visualizations/public/np_ready/public/legacy';
import { LegacyDependenciesPlugin } from './shim';
const plugins: Readonly<TablePluginSetupDependencies> = {
expressions: npSetup.plugins.expressions,
visualizations: visualizationsSetup,
// Temporary solution
// It will be removed when all dependent services are migrated to the new platform.
__LEGACY: new LegacyDependenciesPlugin(),
};
const pluginInstance = plugin({} as PluginInitializerContext);

View file

@ -0,0 +1,46 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { npSetup, npStart } from 'ui/new_platform';
export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
export { AggConfig } from 'ui/vis';
export { AggGroupNames, VisOptionsProps } from 'ui/vis/editors/default';
// @ts-ignore
export { Schemas } from 'ui/vis/editors/default/schemas';
// @ts-ignore
export { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy';
// @ts-ignore
export { PrivateProvider } from 'ui/private/private';
// @ts-ignore
export { PaginateDirectiveProvider } from 'ui/directives/paginate';
// @ts-ignore
export { PaginateControlsDirectiveProvider } from 'ui/directives/paginate';
// @ts-ignore
export { watchMultiDecorator } from 'ui/directives/watch_multi/watch_multi';
// @ts-ignore
export { KbnAccessibleClickProvider } from 'ui/accessibility/kbn_accessible_click';
// @ts-ignore
export { StateManagementConfigProvider } from 'ui/state_management/config_provider';
export { configureAppAngularModule } from 'ui/legacy_compat';
export { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns';
// @ts-ignore
export { tabifyAggResponse } from 'ui/agg_response/tabify';

View file

@ -1,419 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import $ from 'jquery';
describe('Table Vis - Paginated table', function () {
let $el;
let $rootScope;
let $compile;
let $scope;
const defaultPerPage = 10;
const makeData = function (colCount, rowCount) {
let columns = [];
let rows = [];
if (_.isNumber(colCount)) {
_.times(colCount, function (i) {
columns.push({ id: i, title: 'column' + i, formatter: { convert: _.identity } });
});
} else {
columns = colCount.map((col, i) => ({
id: i,
title: col.title,
formatter: col.formatter || { convert: _.identity }
}));
}
if (_.isNumber(rowCount)) {
_.times(rowCount, function (row) {
const rowItems = {};
_.times(columns.length, function (col) {
rowItems[col] = 'item' + col + row;
});
rows.push(rowItems);
});
} else {
rows = rowCount.map(row => {
const newRow = {};
row.forEach((v, i) => newRow[i] = v);
return newRow;
});
}
return {
columns: columns,
rows: rows
};
};
const renderTable = function (table, cols, rows, perPage, sort, linkToTop) {
$scope.table = table || { columns: [], rows: [] };
$scope.cols = cols || [];
$scope.rows = rows || [];
$scope.perPage = perPage || defaultPerPage;
$scope.sort = sort || {};
$scope.linkToTop = linkToTop;
const template = `
<paginated-table
table="table"
columns="cols"
rows="rows"
per-page="perPage"
sort="sort"
link-to-top="linkToTop">`;
$el = $compile(template)($scope);
$scope.$digest();
};
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$scope = $rootScope.$new();
}));
describe('rendering', function () {
it('should not display without rows', function () {
const cols = [{
title: 'test1'
}];
const rows = [];
renderTable(null, cols, rows);
expect($el.children().length).to.be(0);
});
it('should render columns and rows', function () {
const data = makeData(2, 2);
const cols = data.columns;
const rows = data.rows;
renderTable(data, cols, rows);
expect($el.children().length).to.be(1);
const tableRows = $el.find('tbody tr');
// should contain the row data
expect(tableRows.eq(0).find('td').eq(0).text()).to.be(rows[0][0]);
expect(tableRows.eq(0).find('td').eq(1).text()).to.be(rows[0][1]);
expect(tableRows.eq(1).find('td').eq(0).text()).to.be(rows[1][0]);
expect(tableRows.eq(1).find('td').eq(1).text()).to.be(rows[1][1]);
});
it('should paginate rows', function () {
// note: paginate truncates pages, so don't make too many
const rowCount = _.random(16, 24);
const perPageCount = _.random(5, 8);
const data = makeData(3, rowCount);
const pageCount = Math.ceil(rowCount / perPageCount);
renderTable(data, data.columns, data.rows, perPageCount);
const tableRows = $el.find('tbody tr');
expect(tableRows.length).to.be(perPageCount);
// add 2 for the first and last page links
expect($el.find('paginate-controls button').length).to.be(pageCount + 2);
});
it('should not show blank rows on last page', function () {
const rowCount = 7;
const perPageCount = 10;
const data = makeData(3, rowCount);
renderTable(data, data.columns, data.rows, perPageCount, null);
const tableRows = $el.find('tbody tr');
expect(tableRows.length).to.be(rowCount);
});
it('should not show link to top when not set', function () {
const data = makeData(5, 5);
renderTable(data, data.columns, data.rows, 10, null);
const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]');
expect(linkToTop.length).to.be(0);
});
it('should show link to top when set', function () {
const data = makeData(5, 5);
renderTable(data, data.columns, data.rows, 10, null, true);
const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]');
expect(linkToTop.length).to.be(1);
});
});
describe('sorting', function () {
let data;
let lastRowIndex;
let paginatedTable;
beforeEach(function () {
data = makeData(3, [
['bbbb', 'aaaa', 'zzzz'],
['cccc', 'cccc', 'aaaa'],
['zzzz', 'bbbb', 'bbbb'],
['aaaa', 'zzzz', 'cccc'],
]);
lastRowIndex = data.rows.length - 1;
renderTable(data, data.columns, data.rows);
paginatedTable = $el.isolateScope().paginatedTable;
});
// afterEach(function () {
// $scope.$destroy();
// });
it('should not sort by default', function () {
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be(data.rows[0][0]);
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be(data.rows[lastRowIndex][0]);
});
it('should do nothing when sorting by invalid column id', function () {
// sortColumn
paginatedTable.sortColumn(999);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('bbbb');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('aaaa');
expect(tableRows.eq(0).find('td').eq(2).text()).to.be('zzzz');
});
it('should do nothing when sorting by non sortable column', function () {
data.columns[0].sortable = false;
// sortColumn
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('bbbb');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('aaaa');
expect(tableRows.eq(0).find('td').eq(2).text()).to.be('zzzz');
});
it('should set the sort direction to asc when it\'s not explicitly set', function () {
paginatedTable.sortColumn(1);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(2).find('td').eq(1).text()).to.be('cccc');
expect(tableRows.eq(1).find('td').eq(1).text()).to.be('bbbb');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('aaaa');
});
it('should allow you to explicitly set the sort direction', function () {
paginatedTable.sortColumn(1, 'desc');
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('zzzz');
expect(tableRows.eq(1).find('td').eq(1).text()).to.be('cccc');
expect(tableRows.eq(2).find('td').eq(1).text()).to.be('bbbb');
});
it('should sort ascending on first invocation', function () {
// sortColumn
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('aaaa');
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be('zzzz');
});
it('should sort descending on second invocation', function () {
// sortColumn
paginatedTable.sortColumn(0);
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('zzzz');
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be('aaaa');
});
it('should clear sorting on third invocation', function () {
// sortColumn
paginatedTable.sortColumn(0);
paginatedTable.sortColumn(0);
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be(data.rows[0][0]);
expect(tableRows.eq(lastRowIndex).find('td').eq(0).text()).to.be('aaaa');
});
it('should sort new column ascending', function () {
// sort by first column
paginatedTable.sortColumn(0);
$scope.$digest();
// sort by second column
paginatedTable.sortColumn(1);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('aaaa');
expect(tableRows.eq(lastRowIndex).find('td').eq(1).text()).to.be('zzzz');
});
});
describe('sorting duplicate columns', function () {
let data;
let paginatedTable;
const colText = 'test row';
beforeEach(function () {
const cols = [
{ title: colText },
{ title: colText },
{ title: colText }
];
const rows = [
['bbbb', 'aaaa', 'zzzz'],
['cccc', 'cccc', 'aaaa'],
['zzzz', 'bbbb', 'bbbb'],
['aaaa', 'zzzz', 'cccc'],
];
data = makeData(cols, rows);
renderTable(data, data.columns, data.rows);
paginatedTable = $el.isolateScope().paginatedTable;
});
it('should have duplicate column titles', function () {
const columns = $el.find('thead th span');
columns.each(function () {
expect($(this).text()).to.be(colText);
});
});
it('should handle sorting on columns with the same name', function () {
// sort by the last column
paginatedTable.sortColumn(2);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('cccc');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('cccc');
expect(tableRows.eq(0).find('td').eq(2).text()).to.be('aaaa');
expect(tableRows.eq(1).find('td').eq(2).text()).to.be('bbbb');
expect(tableRows.eq(2).find('td').eq(2).text()).to.be('cccc');
expect(tableRows.eq(3).find('td').eq(2).text()).to.be('zzzz');
});
it('should sort correctly between columns', function () {
// sort by the last column
paginatedTable.sortColumn(2);
$scope.$digest();
let tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('cccc');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('cccc');
expect(tableRows.eq(0).find('td').eq(2).text()).to.be('aaaa');
// sort by the first column
paginatedTable.sortColumn(0);
$scope.$digest();
tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('td').eq(0).text()).to.be('aaaa');
expect(tableRows.eq(0).find('td').eq(1).text()).to.be('zzzz');
expect(tableRows.eq(0).find('td').eq(2).text()).to.be('cccc');
expect(tableRows.eq(1).find('td').eq(0).text()).to.be('bbbb');
expect(tableRows.eq(2).find('td').eq(0).text()).to.be('cccc');
expect(tableRows.eq(3).find('td').eq(0).text()).to.be('zzzz');
});
it('should not sort duplicate columns', function () {
paginatedTable.sortColumn(1);
$scope.$digest();
const sorters = $el.find('thead th i');
expect(sorters.eq(0).hasClass('fa-sort')).to.be(true);
expect(sorters.eq(1).hasClass('fa-sort')).to.be(false);
expect(sorters.eq(2).hasClass('fa-sort')).to.be(true);
});
});
describe('object rows', function () {
let cols;
let rows;
let paginatedTable;
beforeEach(function () {
cols = [{
title: 'object test',
id: 0,
formatter: { convert: val => {
return val === 'zzz' ? '<h1>hello</h1>' : val;
} }
}];
rows = [
['aaaa'],
['zzz'],
['bbbb']
];
renderTable({ columns: cols, rows }, cols, rows);
paginatedTable = $el.isolateScope().paginatedTable;
});
it('should append object markup', function () {
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('h1').length).to.be(0);
expect(tableRows.eq(1).find('h1').length).to.be(1);
expect(tableRows.eq(2).find('h1').length).to.be(0);
});
it('should sort using object value', function () {
paginatedTable.sortColumn(0);
$scope.$digest();
let tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('h1').length).to.be(0);
expect(tableRows.eq(1).find('h1').length).to.be(0);
// html row should be the last row
expect(tableRows.eq(2).find('h1').length).to.be(1);
paginatedTable.sortColumn(0);
$scope.$digest();
tableRows = $el.find('tbody tr');
// html row should be the first row
expect(tableRows.eq(0).find('h1').length).to.be(1);
expect(tableRows.eq(1).find('h1').length).to.be(0);
expect(tableRows.eq(2).find('h1').length).to.be(0);
});
});
});

View file

@ -0,0 +1,713 @@
/*
* 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 { isNumber, times, identity, random } from 'lodash';
import angular, { IRootScopeService, IScope, ICompileService } from 'angular';
import $ from 'jquery';
import 'angular-sanitize';
import 'angular-mocks';
import '../table_vis.mock';
import { getAngularModule } from '../get_inner_angular';
import { initTableVisLegacyModule } from '../table_vis_legacy_module';
import { npStart } from '../legacy_imports';
interface Sort {
columnIndex: number;
direction: string;
}
interface Row {
[key: string]: number | string;
}
interface Column {
id?: string;
title: string;
formatter?: {
convert?: (val: string) => string;
};
sortable?: boolean;
}
interface Table {
columns: Column[];
rows: Row[];
}
interface PaginatedTableScope extends IScope {
table?: Table;
cols?: Column[];
rows?: Row[];
perPage?: number;
sort?: Sort;
linkToTop?: boolean;
}
describe('Table Vis - Paginated table', () => {
let $el: JQuery<Element>;
let $rootScope: IRootScopeService;
let $compile: ICompileService;
let $scope: PaginatedTableScope;
const defaultPerPage = 10;
let paginatedTable: any;
const initLocalAngular = () => {
const tableVisModule = getAngularModule('kibana/table_vis', npStart.core);
initTableVisLegacyModule(tableVisModule);
};
beforeEach(initLocalAngular);
beforeEach(angular.mock.module('kibana/table_vis'));
beforeEach(
angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => {
$rootScope = _$rootScope_;
$compile = _$compile_;
$scope = $rootScope.$new();
})
);
afterEach(() => {
$scope.$destroy();
});
const makeData = (colCount: number | Column[], rowCount: number | string[][]) => {
let columns: Column[] = [];
let rows: Row[] = [];
if (isNumber(colCount)) {
times(colCount, i => {
columns.push({ id: `${i}`, title: `column${i}`, formatter: { convert: identity } });
});
} else {
columns = colCount.map(
(col, i) =>
({
id: `${i}`,
title: col.title,
formatter: col.formatter || { convert: identity },
} as Column)
);
}
if (isNumber(rowCount)) {
times(rowCount, row => {
const rowItems: Row = {};
times(columns.length, col => {
rowItems[`${col}`] = `item-${col}-${row}`;
});
rows.push(rowItems);
});
} else {
rows = rowCount.map((row: string[]) => {
const newRow: Row = {};
row.forEach((v, i) => (newRow[i] = v));
return newRow;
});
}
return {
columns,
rows,
};
};
const renderTable = (
table: { columns: Column[]; rows: Row[] } | null,
cols: Column[],
rows: Row[],
perPage?: number,
sort?: Sort,
linkToTop?: boolean
) => {
$scope.table = table || { columns: [], rows: [] };
$scope.cols = cols || [];
$scope.rows = rows || [];
$scope.perPage = perPage || defaultPerPage;
$scope.sort = sort;
$scope.linkToTop = linkToTop;
const template = `
<paginated-table
table="table"
columns="cols"
rows="rows"
per-page="perPage"
sort="sort"
link-to-top="linkToTop">`;
const element = $compile(template)($scope);
$el = $(element);
$scope.$digest();
paginatedTable = element.controller('paginatedTable');
};
describe('rendering', () => {
test('should not display without rows', () => {
const cols: Column[] = [
{
id: 'col-1-1',
title: 'test1',
},
];
const rows: Row[] = [];
renderTable(null, cols, rows);
expect($el.children().length).toBe(0);
});
test('should render columns and rows', () => {
const data = makeData(2, 2);
const cols = data.columns;
const rows = data.rows;
renderTable(data, cols, rows);
expect($el.children().length).toBe(1);
const tableRows = $el.find('tbody tr');
// should contain the row data
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe(rows[0][0]);
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe(rows[0][1]);
expect(
tableRows
.eq(1)
.find('td')
.eq(0)
.text()
).toBe(rows[1][0]);
expect(
tableRows
.eq(1)
.find('td')
.eq(1)
.text()
).toBe(rows[1][1]);
});
test('should paginate rows', () => {
// note: paginate truncates pages, so don't make too many
const rowCount = random(16, 24);
const perPageCount = random(5, 8);
const data = makeData(3, rowCount);
const pageCount = Math.ceil(rowCount / perPageCount);
renderTable(data, data.columns, data.rows, perPageCount);
const tableRows = $el.find('tbody tr');
expect(tableRows.length).toBe(perPageCount);
// add 2 for the first and last page links
expect($el.find('paginate-controls button').length).toBe(pageCount + 2);
});
test('should not show blank rows on last page', () => {
const rowCount = 7;
const perPageCount = 10;
const data = makeData(3, rowCount);
renderTable(data, data.columns, data.rows, perPageCount);
const tableRows = $el.find('tbody tr');
expect(tableRows.length).toBe(rowCount);
});
test('should not show link to top when not set', () => {
const data = makeData(5, 5);
renderTable(data, data.columns, data.rows, 10);
const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]');
expect(linkToTop.length).toBe(0);
});
test('should show link to top when set', () => {
const data = makeData(5, 5);
renderTable(data, data.columns, data.rows, 10, undefined, true);
const linkToTop = $el.find('[data-test-subj="paginateControlsLinkToTop"]');
expect(linkToTop.length).toBe(1);
});
});
describe('sorting', () => {
let data: Table;
let lastRowIndex: number;
beforeEach(() => {
data = makeData(3, [
['bbbb', 'aaaa', 'zzzz'],
['cccc', 'cccc', 'aaaa'],
['zzzz', 'bbbb', 'bbbb'],
['aaaa', 'zzzz', 'cccc'],
]);
lastRowIndex = data.rows.length - 1;
renderTable(data, data.columns, data.rows);
});
test('should not sort by default', () => {
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe(data.rows[0][0]);
expect(
tableRows
.eq(lastRowIndex)
.find('td')
.eq(0)
.text()
).toBe(data.rows[lastRowIndex][0]);
});
test('should do nothing when sorting by invalid column id', () => {
// sortColumn
paginatedTable.sortColumn(999);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe('bbbb');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('aaaa');
expect(
tableRows
.eq(0)
.find('td')
.eq(2)
.text()
).toBe('zzzz');
});
test('should do nothing when sorting by non sortable column', () => {
data.columns[0].sortable = false;
// sortColumn
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe('bbbb');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('aaaa');
expect(
tableRows
.eq(0)
.find('td')
.eq(2)
.text()
).toBe('zzzz');
});
test("should set the sort direction to asc when it's not explicitly set", () => {
paginatedTable.sortColumn(1);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(2)
.find('td')
.eq(1)
.text()
).toBe('cccc');
expect(
tableRows
.eq(1)
.find('td')
.eq(1)
.text()
).toBe('bbbb');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('aaaa');
});
test('should allow you to explicitly set the sort direction', () => {
paginatedTable.sortColumn(1, 'desc');
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('zzzz');
expect(
tableRows
.eq(1)
.find('td')
.eq(1)
.text()
).toBe('cccc');
expect(
tableRows
.eq(2)
.find('td')
.eq(1)
.text()
).toBe('bbbb');
});
test('should sort ascending on first invocation', () => {
// sortColumn
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe('aaaa');
expect(
tableRows
.eq(lastRowIndex)
.find('td')
.eq(0)
.text()
).toBe('zzzz');
});
test('should sort descending on second invocation', () => {
// sortColumn
paginatedTable.sortColumn(0);
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe('zzzz');
expect(
tableRows
.eq(lastRowIndex)
.find('td')
.eq(0)
.text()
).toBe('aaaa');
});
test('should clear sorting on third invocation', () => {
// sortColumn
paginatedTable.sortColumn(0);
paginatedTable.sortColumn(0);
paginatedTable.sortColumn(0);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe(data.rows[0][0]);
expect(
tableRows
.eq(lastRowIndex)
.find('td')
.eq(0)
.text()
).toBe('aaaa');
});
test('should sort new column ascending', () => {
// sort by first column
paginatedTable.sortColumn(0);
$scope.$digest();
// sort by second column
paginatedTable.sortColumn(1);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('aaaa');
expect(
tableRows
.eq(lastRowIndex)
.find('td')
.eq(1)
.text()
).toBe('zzzz');
});
});
describe('sorting duplicate columns', () => {
let data;
const colText = 'test row';
beforeEach(() => {
const cols: Column[] = [{ title: colText }, { title: colText }, { title: colText }];
const rows = [
['bbbb', 'aaaa', 'zzzz'],
['cccc', 'cccc', 'aaaa'],
['zzzz', 'bbbb', 'bbbb'],
['aaaa', 'zzzz', 'cccc'],
];
data = makeData(cols, rows);
renderTable(data, data.columns, data.rows);
});
test('should have duplicate column titles', () => {
const columns = $el.find('thead th span');
columns.each((i, col) => {
expect($(col).text()).toBe(colText);
});
});
test('should handle sorting on columns with the same name', () => {
// sort by the last column
paginatedTable.sortColumn(2);
$scope.$digest();
const tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe('cccc');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('cccc');
expect(
tableRows
.eq(0)
.find('td')
.eq(2)
.text()
).toBe('aaaa');
expect(
tableRows
.eq(1)
.find('td')
.eq(2)
.text()
).toBe('bbbb');
expect(
tableRows
.eq(2)
.find('td')
.eq(2)
.text()
).toBe('cccc');
expect(
tableRows
.eq(3)
.find('td')
.eq(2)
.text()
).toBe('zzzz');
});
test('should sort correctly between columns', () => {
// sort by the last column
paginatedTable.sortColumn(2);
$scope.$digest();
let tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe('cccc');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('cccc');
expect(
tableRows
.eq(0)
.find('td')
.eq(2)
.text()
).toBe('aaaa');
// sort by the first column
paginatedTable.sortColumn(0);
$scope.$digest();
tableRows = $el.find('tbody tr');
expect(
tableRows
.eq(0)
.find('td')
.eq(0)
.text()
).toBe('aaaa');
expect(
tableRows
.eq(0)
.find('td')
.eq(1)
.text()
).toBe('zzzz');
expect(
tableRows
.eq(0)
.find('td')
.eq(2)
.text()
).toBe('cccc');
expect(
tableRows
.eq(1)
.find('td')
.eq(0)
.text()
).toBe('bbbb');
expect(
tableRows
.eq(2)
.find('td')
.eq(0)
.text()
).toBe('cccc');
expect(
tableRows
.eq(3)
.find('td')
.eq(0)
.text()
).toBe('zzzz');
});
test('should not sort duplicate columns', () => {
paginatedTable.sortColumn(1);
$scope.$digest();
const sorters = $el.find('thead th i');
expect(sorters.eq(0).hasClass('fa-sort')).toBe(true);
expect(sorters.eq(1).hasClass('fa-sort')).toBe(false);
expect(sorters.eq(2).hasClass('fa-sort')).toBe(true);
});
});
describe('object rows', () => {
let cols: Column[];
let rows: any;
beforeEach(() => {
cols = [
{
title: 'object test',
id: '0',
formatter: {
convert: val => {
return val === 'zzz' ? '<h1>hello</h1>' : val;
},
},
},
];
rows = [['aaaa'], ['zzz'], ['bbbb']];
renderTable({ columns: cols, rows }, cols, rows);
});
test('should append object markup', () => {
const tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('h1').length).toBe(0);
expect(tableRows.eq(1).find('h1').length).toBe(1);
expect(tableRows.eq(2).find('h1').length).toBe(0);
});
test('should sort using object value', () => {
paginatedTable.sortColumn(0);
$scope.$digest();
let tableRows = $el.find('tbody tr');
expect(tableRows.eq(0).find('h1').length).toBe(0);
expect(tableRows.eq(1).find('h1').length).toBe(0);
// html row should be the last row
expect(tableRows.eq(2).find('h1').length).toBe(1);
paginatedTable.sortColumn(0);
$scope.$digest();
tableRows = $el.find('tbody tr');
// html row should be the first row
expect(tableRows.eq(0).find('h1').length).toBe(1);
expect(tableRows.eq(1).find('h1').length).toBe(0);
expect(tableRows.eq(2).find('h1').length).toBe(0);
});
});
});

View file

@ -21,8 +21,6 @@ import { VisualizationsSetup } from '../../visualizations/public';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public';
import { LegacyDependenciesPlugin } from './shim';
import { createTableVisFn } from './table_vis_fn';
import { tableVisTypeDefinition } from './table_vis_type';
@ -30,7 +28,6 @@ import { tableVisTypeDefinition } from './table_vis_type';
export interface TablePluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
__LEGACY: LegacyDependenciesPlugin;
}
/** @internal */
@ -43,9 +40,8 @@ export class TableVisPlugin implements Plugin<Promise<void>, void> {
public async setup(
core: CoreSetup,
{ expressions, visualizations, __LEGACY }: TablePluginSetupDependencies
{ expressions, visualizations }: TablePluginSetupDependencies
) {
__LEGACY.setup();
expressions.registerFunction(createTableVisFn);
visualizations.types.createBaseVisualization(tableVisTypeDefinition);

View file

@ -1,20 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './legacy_dependencies_plugin';

View file

@ -17,16 +17,30 @@
* under the License.
*/
import { CoreStart, Plugin } from '../../../../../core/public';
import { initTableVisLegacyModule } from './table_vis_legacy_module';
import { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers';
import { StubBrowserStorage } from 'test_utils/stub_browser_storage';
import { injectedMetadataServiceMock } from '../../../../core/public/mocks';
/** @internal */
export class LegacyDependenciesPlugin implements Plugin {
public setup() {
initTableVisLegacyModule();
}
jest.doMock('ui/new_platform', () => {
const npMock = createUiNewPlatformMock();
return {
npSetup: {
...npMock.npSetup,
core: {
...npMock.npSetup.core,
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
},
},
npStart: {
...npMock.npStart,
core: {
...npMock.npStart.core,
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
},
},
};
});
public start(core: CoreStart) {
// nothing to do here yet
}
}
Object.assign(window, {
sessionStorage: new StubBrowserStorage(),
});

View file

@ -0,0 +1,257 @@
/*
* 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 angular, { IRootScopeService, IScope, ICompileService } from 'angular';
import 'angular-mocks';
import 'angular-sanitize';
import $ from 'jquery';
import './table_vis.mock';
// @ts-ignore
import StubIndexPattern from 'test_utils/stub_index_pattern';
import { getAngularModule } from './get_inner_angular';
import { initTableVisLegacyModule } from './table_vis_legacy_module';
import {
npStart,
legacyResponseHandlerProvider,
AggConfig,
tabifyAggResponse,
} from './legacy_imports';
import { tableVisTypeDefinition } from './table_vis_type';
import { Vis } from '../../visualizations/public';
import { setup as visualizationsSetup } from '../../visualizations/public/np_ready/public/legacy';
// eslint-disable-next-line
import { stubFields } from '../../../../plugins/data/public/stubs';
// eslint-disable-next-line
import { setFieldFormats } from '../../../../plugins/data/public/index_patterns/services';
interface TableVisScope extends IScope {
[key: string]: any;
}
const oneRangeBucket = {
hits: {
total: 6039,
max_score: 0,
hits: [],
},
aggregations: {
agg_2: {
buckets: {
'0.0-1000.0': {
from: 0,
from_as_string: '0.0',
to: 1000,
to_as_string: '1000.0',
doc_count: 606,
},
'1000.0-2000.0': {
from: 1000,
from_as_string: '1000.0',
to: 2000,
to_as_string: '2000.0',
doc_count: 298,
},
},
},
},
};
describe('Table Vis - Controller', () => {
let $rootScope: IRootScopeService & { [key: string]: any };
let $compile: ICompileService;
let $scope: TableVisScope;
let $el: JQuery<HTMLElement>;
let tableAggResponse: any;
let tabifiedResponse: any;
let stubIndexPattern: any;
const initLocalAngular = () => {
const tableVisModule = getAngularModule('kibana/table_vis', npStart.core);
initTableVisLegacyModule(tableVisModule);
};
beforeEach(initLocalAngular);
beforeAll(() => {
visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition);
});
beforeEach(angular.mock.module('kibana/table_vis'));
beforeEach(
angular.mock.inject((_$rootScope_: IRootScopeService, _$compile_: ICompileService) => {
$rootScope = _$rootScope_;
$compile = _$compile_;
tableAggResponse = legacyResponseHandlerProvider().handler;
})
);
beforeEach(() => {
setFieldFormats(({
getDefaultInstance: jest.fn(),
} as unknown) as any);
stubIndexPattern = new StubIndexPattern(
'logstash-*',
(cfg: any) => cfg,
'time',
stubFields,
npStart.core.uiSettings
);
});
function getRangeVis(params?: object) {
// @ts-ignore
return new Vis(stubIndexPattern, {
type: 'table',
params: params || {},
aggs: [
{ type: 'count', schema: 'metric' },
{
type: 'range',
schema: 'bucket',
params: {
field: 'bytes',
ranges: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 },
],
},
},
],
});
}
const dimensions = {
buckets: [
{
accessor: 0,
},
],
metrics: [
{
accessor: 1,
format: { id: 'range' },
},
],
};
// basically a parameterized beforeEach
function initController(vis: Vis) {
vis.aggs.aggs.forEach((agg: AggConfig, i: number) => {
agg.id = 'agg_' + (i + 1);
});
tabifiedResponse = tabifyAggResponse(vis.aggs, oneRangeBucket);
$rootScope.vis = vis;
$rootScope.visParams = vis.params;
$rootScope.uiState = {
get: jest.fn(),
set: jest.fn(),
};
$rootScope.renderComplete = () => {};
$rootScope.newScope = (scope: TableVisScope) => {
$scope = scope;
};
$el = $('<div>')
.attr('ng-controller', 'KbnTableVisController')
.attr('ng-init', 'newScope(this)');
$compile($el)($rootScope);
}
// put a response into the controller
function attachEsResponseToScope(resp: object) {
$rootScope.esResponse = resp;
$rootScope.$apply();
}
// remove the response from the controller
function removeEsResponseFromScope() {
delete $rootScope.esResponse;
$rootScope.renderComplete = () => {};
$rootScope.$apply();
}
test('exposes #tableGroups and #hasSomeRows when a response is attached to scope', async () => {
const vis: Vis = getRangeVis();
initController(vis);
expect(!$scope.tableGroups).toBeTruthy();
expect(!$scope.hasSomeRows).toBeTruthy();
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
expect($scope.hasSomeRows).toBeTruthy();
expect($scope.tableGroups.tables).toBeDefined();
expect($scope.tableGroups.tables.length).toBe(1);
expect($scope.tableGroups.tables[0].columns.length).toBe(2);
expect($scope.tableGroups.tables[0].rows.length).toBe(2);
});
test('clears #tableGroups and #hasSomeRows when the response is removed', async () => {
const vis = getRangeVis();
initController(vis);
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
removeEsResponseFromScope();
expect(!$scope.hasSomeRows).toBeTruthy();
expect(!$scope.tableGroups).toBeTruthy();
});
test('sets the sort on the scope when it is passed as a vis param', async () => {
const sortObj = {
columnIndex: 1,
direction: 'asc',
};
const vis = getRangeVis({ sort: sortObj });
initController(vis);
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
expect($scope.sort.columnIndex).toEqual(sortObj.columnIndex);
expect($scope.sort.direction).toEqual(sortObj.direction);
});
test('sets #hasSomeRows properly if the table group is empty', async () => {
const vis = getRangeVis();
initController(vis);
tabifiedResponse.rows = [];
attachEsResponseToScope(await tableAggResponse(tabifiedResponse, dimensions));
expect($scope.hasSomeRows).toBeFalsy();
expect(!$scope.tableGroups).toBeTruthy();
});
test('passes partialRows:true to tabify based on the vis params', () => {
const vis = getRangeVis({ showPartialRows: true });
initController(vis);
expect(vis.isHierarchical()).toEqual(true);
});
test('passes partialRows:false to tabify based on the vis params', () => {
const vis = getRangeVis({ showPartialRows: false });
initController(vis);
expect(vis.isHierarchical()).toEqual(false);
});
});

View file

@ -22,9 +22,7 @@ import { createTableVisFn } from './table_vis_fn';
// eslint-disable-next-line
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
jest.mock('ui/new_platform');
jest.mock('ui/vis/response_handlers/legacy', () => {
jest.mock('./legacy_imports', () => {
const mockResponseHandler = jest.fn().mockReturnValue(
Promise.resolve({
tables: [{ columns: [], rows: [] }],
@ -37,7 +35,7 @@ jest.mock('ui/vis/response_handlers/legacy', () => {
};
});
const { mockResponseHandler } = jest.requireMock('ui/vis/response_handlers/legacy');
const { mockResponseHandler } = jest.requireMock('./legacy_imports');
describe('interpreter/functions#table', () => {
const fn = functionWrapper(createTableVisFn);

View file

@ -17,32 +17,25 @@
* under the License.
*/
import { once } from 'lodash';
import { IModule } from 'angular';
// @ts-ignore
import { uiModules } from 'ui/modules';
import 'angular-recursion';
import 'ui/directives/paginate';
import { TableVisController } from './table_vis_controller.js';
// @ts-ignore
import { TableVisController } from '../table_vis_controller.js';
import { KbnAggTable } from './agg_table/agg_table';
// @ts-ignore
import { KbnAggTable } from '../agg_table/agg_table';
import { KbnAggTableGroup } from './agg_table/agg_table_group';
// @ts-ignore
import { KbnAggTableGroup } from '../agg_table/agg_table_group';
import { KbnRows } from './paginated_table/rows';
// @ts-ignore
import { KbnRows } from '../paginated_table/rows';
// @ts-ignore
import { PaginatedTable } from '../paginated_table/paginated_table';
import { PaginatedTable } from './paginated_table/paginated_table';
/** @internal */
export const initTableVisLegacyModule = once((): void => {
uiModules
.get('kibana/table_vis', ['kibana', 'RecursionHelper'])
export const initTableVisLegacyModule = (angularIns: IModule): void => {
angularIns
.controller('KbnTableVisController', TableVisController)
.directive('kbnAggTable', KbnAggTable)
.directive('kbnAggTableGroup', KbnAggTableGroup)
.directive('kbnRows', KbnRows)
.directive('paginatedTable', PaginatedTable);
});
};

View file

@ -17,7 +17,6 @@
* under the License.
*/
// @ts-ignore
import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy';
import { legacyResponseHandlerProvider } from './legacy_imports';
export const tableVisResponseHandler = legacyResponseHandlerProvider().handler;

View file

@ -18,18 +18,13 @@
*/
import { i18n } from '@kbn/i18n';
import { Vis } from 'ui/vis';
// @ts-ignore
// @ts-ignore
import { Schemas } from 'ui/vis/editors/default/schemas';
// @ts-ignore
import { AngularVisController } from 'ui/vis/vis_types/angular_vis_type';
import { AggGroupNames } from 'ui/vis/editors/default';
import { AggGroupNames, Schemas } from './legacy_imports';
import { Vis } from '../../visualizations/public';
import { tableVisResponseHandler } from './table_vis_request_handler';
// @ts-ignore
import tableVisTemplate from './table_vis.html';
import { TableOptions } from './components/table_vis_options';
import { TableVisualizationController } from './vis_controller';
export const tableVisTypeDefinition = {
type: 'table',
@ -41,7 +36,7 @@ export const tableVisTypeDefinition = {
description: i18n.translate('visTypeTable.tableVisDescription', {
defaultMessage: 'Display values in a table',
}),
visualization: AngularVisController,
visualization: TableVisualizationController,
visConfig: {
defaults: {
perPage: 10,

View file

@ -0,0 +1,104 @@
/*
* 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 angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular';
import $ from 'jquery';
import { Vis, VisParams } from '../../visualizations/public';
import { npStart } from './legacy_imports';
import { getAngularModule } from './get_inner_angular';
import { initTableVisLegacyModule } from './table_vis_legacy_module';
const innerAngularName = 'kibana/table_vis';
export class TableVisualizationController {
private tableVisModule: IModule | undefined;
private injector: auto.IInjectorService | undefined;
el: JQuery<Element>;
vis: Vis;
$rootScope: IRootScopeService | null = null;
$scope: (IScope & { [key: string]: any }) | undefined;
$compile: ICompileService | undefined;
constructor(domeElement: Element, vis: Vis) {
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;
}
initLocalAngular() {
if (!this.tableVisModule) {
this.tableVisModule = getAngularModule(innerAngularName, npStart.core);
initTableVisLegacyModule(this.tableVisModule);
}
}
async render(esResponse: object, visParams: VisParams, status: { [key: string]: boolean }) {
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;
}
this.$scope.vis = this.vis;
this.$scope.visState = this.vis.getState();
this.$scope.esResponse = esResponse;
this.$scope.visParams = visParams;
this.$scope.renderComplete = resolve;
this.$scope.renderFailed = reject;
this.$scope.resize = Date.now();
this.$scope.updateStatus = status;
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;
}
}
}

View file

@ -22,209 +22,215 @@ import { i18n } from '@kbn/i18n';
import { uiModules } from '../modules';
import paginateControlsTemplate from './partials/paginate_controls.html';
uiModules.get('kibana')
.directive('paginate', function ($parse, $compile) {
return {
restrict: 'E',
scope: true,
link: {
pre: function ($scope, $el, attrs) {
if (_.isUndefined(attrs.bottomControls)) attrs.bottomControls = true;
if ($el.find('paginate-controls.paginate-bottom').length === 0 && attrs.bottomControls) {
$el.append($compile('<paginate-controls class="paginate-bottom">')($scope));
}
},
post: function ($scope, $el, attrs) {
if (_.isUndefined(attrs.topControls)) attrs.topControls = false;
if ($el.find('paginate-controls.paginate-top').length === 0 && attrs.topControls) {
$el.prepend($compile('<paginate-controls class="paginate-top">')($scope));
}
const paginate = $scope.paginate;
// add some getters to the controller powered by attributes
paginate.getList = $parse(attrs.list);
paginate.perPageProp = attrs.perPageProp;
if (attrs.perPage) {
paginate.perPage = attrs.perPage;
$scope.showSelector = false;
} else {
$scope.showSelector = true;
}
paginate.otherWidthGetter = $parse(attrs.otherWidth);
paginate.init();
export function PaginateDirectiveProvider($parse, $compile) {
return {
restrict: 'E',
scope: true,
link: {
pre: function ($scope, $el, attrs) {
if (_.isUndefined(attrs.bottomControls)) attrs.bottomControls = true;
if ($el.find('paginate-controls.paginate-bottom').length === 0 && attrs.bottomControls) {
$el.append($compile('<paginate-controls class="paginate-bottom">')($scope));
}
},
controllerAs: 'paginate',
controller: function ($scope, $document) {
const self = this;
const ALL = 0;
const allSizeTitle = i18n.translate('common.ui.directives.paginate.size.allDropDownOptionLabel', {
post: function ($scope, $el, attrs) {
if (_.isUndefined(attrs.topControls)) attrs.topControls = false;
if ($el.find('paginate-controls.paginate-top').length === 0 && attrs.topControls) {
$el.prepend($compile('<paginate-controls class="paginate-top">')($scope));
}
const paginate = $scope.paginate;
// add some getters to the controller powered by attributes
paginate.getList = $parse(attrs.list);
paginate.perPageProp = attrs.perPageProp;
if (attrs.perPage) {
paginate.perPage = attrs.perPage;
$scope.showSelector = false;
} else {
$scope.showSelector = true;
}
paginate.otherWidthGetter = $parse(attrs.otherWidth);
paginate.init();
},
},
controllerAs: 'paginate',
controller: function ($scope, $document) {
const self = this;
const ALL = 0;
const allSizeTitle = i18n.translate(
'common.ui.directives.paginate.size.allDropDownOptionLabel',
{
defaultMessage: 'All',
});
}
);
self.sizeOptions = [
{ title: '10', value: 10 },
{ title: '25', value: 25 },
{ title: '100', value: 100 },
{ title: allSizeTitle, value: ALL }
];
self.sizeOptions = [
{ title: '10', value: 10 },
{ title: '25', value: 25 },
{ title: '100', value: 100 },
{ title: allSizeTitle, value: ALL },
];
// setup the watchers, called in the post-link function
self.init = function () {
// setup the watchers, called in the post-link function
self.init = function () {
self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp];
self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp];
$scope.$watchMulti(['paginate.perPage', self.perPageProp, self.otherWidthGetter], function (
vals,
oldVals
) {
const intChanges = vals[0] !== oldVals[0];
$scope.$watchMulti([
'paginate.perPage',
self.perPageProp,
self.otherWidthGetter
], function (vals, oldVals) {
const intChanges = vals[0] !== oldVals[0];
if (intChanges) {
if (!setPerPage(self.perPage)) {
if (intChanges) {
if (!setPerPage(self.perPage)) {
// if we are not able to set the external value,
// render now, otherwise wait for the external value
// to trigger the watcher again
self.renderList();
}
return;
self.renderList();
}
self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp];
if (self.perPage == null) {
self.perPage = ALL;
return;
}
self.renderList();
});
$scope.$watch('page', self.changePage);
$scope.$watchCollection(self.getList, function (list) {
$scope.list = list;
self.renderList();
});
};
self.goToPage = function (number) {
if (number) {
if (number.hasOwnProperty('number')) number = number.number;
$scope.page = $scope.pages[number - 1] || $scope.pages[0];
}
};
self.goToTop = function goToTop() {
$document.scrollTop(0);
};
self.renderList = function () {
$scope.pages = [];
if (!$scope.list) return;
const perPage = _.parseInt(self.perPage);
const count = perPage ? Math.ceil($scope.list.length / perPage) : 1;
_.times(count, function (i) {
let page;
if (perPage) {
const start = perPage * i;
page = $scope.list.slice(start, start + perPage);
} else {
page = $scope.list.slice(0);
}
page.number = i + 1;
page.i = i;
page.count = count;
page.first = page.number === 1;
page.last = page.number === count;
page.firstItem = (page.number - 1) * perPage + 1;
page.lastItem = Math.min(page.number * perPage, $scope.list.length);
page.prev = $scope.pages[i - 1];
if (page.prev) page.prev.next = page;
$scope.pages.push(page);
});
// set the new page, or restore the previous page number
if ($scope.page && $scope.page.i < $scope.pages.length) {
$scope.page = $scope.pages[$scope.page.i];
} else {
$scope.page = $scope.pages[0];
}
if ($scope.page && $scope.onPageChanged) {
$scope.onPageChanged($scope.page);
}
};
self.changePage = function (page) {
if (!page) {
$scope.otherPages = null;
return;
}
// setup the list of the other pages to link to
$scope.otherPages = [];
const width = +self.otherWidthGetter($scope) || 5;
let left = page.i - Math.round((width - 1) / 2);
let right = left + width - 1;
// shift neg count from left to right
if (left < 0) {
right += 0 - left;
left = 0;
self.perPage = _.parseInt(self.perPage) || $scope[self.perPageProp];
if (self.perPage == null) {
self.perPage = ALL;
return;
}
// shift extra right nums to left
const lastI = page.count - 1;
if (right > lastI) {
right = lastI;
left = right - width + 1;
self.renderList();
});
$scope.$watch('page', self.changePage);
$scope.$watchCollection(self.getList, function (list) {
$scope.list = list;
self.renderList();
});
};
self.goToPage = function (number) {
if (number) {
if (number.hasOwnProperty('number')) number = number.number;
$scope.page = $scope.pages[number - 1] || $scope.pages[0];
}
};
self.goToTop = function goToTop() {
$document.scrollTop(0);
};
self.renderList = function () {
$scope.pages = [];
if (!$scope.list) return;
const perPage = _.parseInt(self.perPage);
const count = perPage ? Math.ceil($scope.list.length / perPage) : 1;
_.times(count, function (i) {
let page;
if (perPage) {
const start = perPage * i;
page = $scope.list.slice(start, start + perPage);
} else {
page = $scope.list.slice(0);
}
for (let i = left; i <= right; i++) {
const other = $scope.pages[i];
page.number = i + 1;
page.i = i;
if (!other) continue;
page.count = count;
page.first = page.number === 1;
page.last = page.number === count;
page.firstItem = (page.number - 1) * perPage + 1;
page.lastItem = Math.min(page.number * perPage, $scope.list.length);
$scope.otherPages.push(other);
if (other.last) $scope.otherPages.containsLast = true;
if (other.first) $scope.otherPages.containsFirst = true;
}
page.prev = $scope.pages[i - 1];
if (page.prev) page.prev.next = page;
if ($scope.onPageChanged) {
$scope.onPageChanged($scope.page);
}
};
$scope.pages.push(page);
});
function setPerPage(val) {
let $ppParent = $scope;
// set the new page, or restore the previous page number
if ($scope.page && $scope.page.i < $scope.pages.length) {
$scope.page = $scope.pages[$scope.page.i];
} else {
$scope.page = $scope.pages[0];
}
while ($ppParent && !_.has($ppParent, self.perPageProp)) {
$ppParent = $ppParent.$parent;
}
if ($scope.page && $scope.onPageChanged) {
$scope.onPageChanged($scope.page);
}
};
if ($ppParent) {
$ppParent[self.perPageProp] = val;
return true;
}
self.changePage = function (page) {
if (!page) {
$scope.otherPages = null;
return;
}
// setup the list of the other pages to link to
$scope.otherPages = [];
const width = +self.otherWidthGetter($scope) || 5;
let left = page.i - Math.round((width - 1) / 2);
let right = left + width - 1;
// shift neg count from left to right
if (left < 0) {
right += 0 - left;
left = 0;
}
// shift extra right nums to left
const lastI = page.count - 1;
if (right > lastI) {
right = lastI;
left = right - width + 1;
}
for (let i = left; i <= right; i++) {
const other = $scope.pages[i];
if (!other) continue;
$scope.otherPages.push(other);
if (other.last) $scope.otherPages.containsLast = true;
if (other.first) $scope.otherPages.containsFirst = true;
}
if ($scope.onPageChanged) {
$scope.onPageChanged($scope.page);
}
};
function setPerPage(val) {
let $ppParent = $scope;
while ($ppParent && !_.has($ppParent, self.perPageProp)) {
$ppParent = $ppParent.$parent;
}
if ($ppParent) {
$ppParent[self.perPageProp] = val;
return true;
}
}
};
})
.directive('paginateControls', function () {
},
};
}
export function PaginateControlsDirectiveProvider() {
// this directive is automatically added by paginate if not found within it's $el
return {
restrict: 'E',
template: paginateControlsTemplate
};
});
return {
restrict: 'E',
template: paginateControlsTemplate,
};
}
uiModules
.get('kibana')
.directive('paginate', PaginateDirectiveProvider)
.directive('paginateControls', PaginateControlsDirectiveProvider);

View file

@ -114,8 +114,10 @@ export const npSetup = {
registerAction: sinon.fake(),
registerTrigger: sinon.fake(),
},
feature_catalogue: {
register: sinon.fake(),
home: {
featureCatalogue: {
register: sinon.fake(),
},
},
},
};
@ -229,8 +231,10 @@ export const npStart = {
getTriggerActions: sinon.fake(),
getTriggerCompatibleActions: sinon.fake(),
},
feature_catalogue: {
register: sinon.fake(),
home: {
featureCatalogue: {
register: sinon.fake(),
},
},
},
};