Removes the angular dependency from tag cloud (#15779) (#17182)

- Implements tag_cloud as a Base Visualization
- Use React for labels
- Introduce screenshot comparison unit tests
This commit is contained in:
Thomas Neirynck 2018-03-15 15:48:50 -04:00 committed by GitHub
parent af8a04c5f1
commit 46641f942e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 333 additions and 100 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,156 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import * as visModule from 'ui/vis';
import { ImageComparator } from 'test_utils/image_comparator';
import { TagCloudVisualization } from '../tag_cloud_visualization';
import basicdrawPng from './basicdraw.png';
import afterresizePng from './afterresize.png';
import afterparamChange from './afterparamchange.png';
const THRESHOLD = 0.65;
const PIXEL_DIFF = 64;
describe('TagCloudVisualizationTest', function () {
let domNode;
let Vis;
let indexPattern;
let vis;
let imageComparator;
const dummyTableGroup = {
tables: [
{
columns: [{
'aggConfig': {
'id': '2',
'enabled': true,
'type': 'terms',
'schema': 'segment',
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' },
fieldFormatter: () => (x => x)
}, 'title': 'geo.dest: Descending'
}, {
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
'title': 'Count'
}],
rows: [['CN', 26], ['IN', 17], ['US', 6], ['DE', 4], ['BR', 3]]
}
]
};
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject((Private) => {
Vis = Private(visModule.VisProvider);
indexPattern = Private(LogstashIndexPatternStubProvider);
}));
describe('TagCloudVisualization - basics', function () {
beforeEach(async function () {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
vis = new Vis(indexPattern, {
type: 'tagcloud'
});
});
afterEach(function () {
teardownDOM();
imageComparator.destroy();
});
it('simple draw', async function () {
const tagcloudVisualization = new TagCloudVisualization(domNode, vis);
await tagcloudVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
const mismatchedPixels = await imageComparator.compareDOMContents(domNode.innerHTML, 512, 512, basicdrawPng, THRESHOLD);
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
});
it('with resize', async function () {
const tagcloudVisualization = new TagCloudVisualization(domNode, vis);
await tagcloudVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
domNode.style.width = '256px';
domNode.style.height = '368px';
await tagcloudVisualization.render(dummyTableGroup, {
resize: true,
params: false,
aggs: false,
data: false,
uiState: false
});
const mismatchedPixels = await imageComparator.compareDOMContents(domNode.innerHTML, 256, 368, afterresizePng, THRESHOLD);
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
});
it('with param change', async function () {
const tagcloudVisualization = new TagCloudVisualization(domNode, vis);
await tagcloudVisualization.render(dummyTableGroup, {
resize: false,
params: true,
aggs: true,
data: true,
uiState: false
});
domNode.style.width = '256px';
domNode.style.height = '368px';
vis.params.orientation = 'right angled';
vis.params.minFontSize = 70;
await tagcloudVisualization.render(dummyTableGroup, {
resize: true,
params: true,
aggs: false,
data: false,
uiState: false
});
const mismatchedPixels = await imageComparator.compareDOMContents(domNode.innerHTML, 256, 368, afterparamChange, THRESHOLD);
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
});
});
function setupDOM(width, height) {
domNode = document.createElement('div');
domNode.style.top = '0';
domNode.style.left = '0';
domNode.style.width = width;
domNode.style.height = height;
domNode.style.position = 'fixed';
domNode.style.border = '1px solid blue';
domNode.style['pointer-events'] = 'none';
document.body.appendChild(domNode);
}
function teardownDOM() {
domNode.innerHTML = '';
document.body.removeChild(domNode);
}
});

View file

@ -0,0 +1,22 @@
import React, { Component } from 'react';
export class FeedbackMessage extends Component {
constructor() {
super();
this.state = { shouldShowTruncate: false, shouldShowIncomplete: false };
}
render() {
return (
<div className="tagcloud-notifications" >
<div className="tagcloud-truncated-message" style={{ display: this.state.shouldShowTruncate ? 'block' : 'none' }}>
The number of tags has been truncated to avoid long draw times.
</div>
<div className="tagcloud-incomplete-message" style={{ display: this.state.shouldShowIncomplete ? 'block' : 'none' }}>
The container is too small to display the entire cloud. Tags might be cropped or omitted.
</div>
</div>
);
}
}

View file

@ -0,0 +1,18 @@
import React, { Component } from 'react';
export class Label extends Component {
constructor() {
super();
this.state = { label: '', shouldShowLabel: true };
}
render() {
return (
<div
className="tagcloud-custom-label"
style={{ display: this.state.shouldShowLabel ? 'block' : 'none' }}
>{this.state.label}
</div>
);
}
}

View file

@ -1,7 +0,0 @@
<div ng-controller="KbnTagCloudController" class="tagcloud-vis">
<div ng-show="vis.params.hideLabel" class="tagcloud-custom-label"></div>
<div class="tagcloud-notifications">
<div class="tagcloud-truncated-message">The number of tags has been truncated to avoid long draw times.</div>
<div class="tagcloud-incomplete-message">The container is too small to display the entire cloud. Tags might be cropped or omitted.</div>
</div>
</div>

View file

@ -1,83 +0,0 @@
import { uiModules } from 'ui/modules';
import TagCloud from 'plugins/tagcloud/tag_cloud';
const module = uiModules.get('kibana/tagcloud', ['kibana']);
module.controller('KbnTagCloudController', function ($scope, $element) {
const containerNode = $element[0];
const maxTagCount = 200;
let truncated = false;
let bucketAgg;
const tagCloud = new TagCloud(containerNode);
tagCloud.on('select', (event) => {
if (!bucketAgg) return;
const filter = bucketAgg.createFilter(event);
$scope.vis.API.queryFilter.addFilters(filter);
});
tagCloud.on('renderComplete', () => {
const truncatedMessage = containerNode.querySelector('.tagcloud-truncated-message');
const incompleteMessage = containerNode.querySelector('.tagcloud-incomplete-message');
if (!$scope.vis.aggs[0] || !$scope.vis.aggs[1]) {
incompleteMessage.style.display = 'none';
truncatedMessage.style.display = 'none';
$scope.renderComplete();
return;
}
const bucketName = containerNode.querySelector('.tagcloud-custom-label');
bucketName.textContent = `${$scope.vis.aggs[0].makeLabel()} - ${$scope.vis.aggs[1].makeLabel()}`;
truncatedMessage.style.display = truncated ? 'block' : 'none';
const status = tagCloud.getStatus();
if (TagCloud.STATUS.COMPLETE === status) {
incompleteMessage.style.display = 'none';
} else if (TagCloud.STATUS.INCOMPLETE === status) {
incompleteMessage.style.display = 'block';
}
$scope.renderComplete();
});
$scope.$watch('renderComplete', async function () {
if ($scope.updateStatus.resize) {
tagCloud.resize();
}
if ($scope.updateStatus.params) {
tagCloud.setOptions($scope.vis.params);
}
if (!$scope.esResponse || !$scope.esResponse.tables.length) {
tagCloud.setData([]);
return;
}
const data = $scope.esResponse.tables[0];
bucketAgg = data.columns[0].aggConfig;
const tags = data.rows.map(row => {
const [tag, count] = row;
return {
displayText: bucketAgg ? bucketAgg.fieldFormatter()(tag) : tag,
rawText: tag,
value: count
};
});
if (tags.length > maxTagCount) {
tags.length = maxTagCount;
truncated = true;
} else {
truncated = false;
}
tagCloud.setData(tags);
});
});

View file

@ -1,19 +1,19 @@
import 'plugins/tagcloud/tag_cloud.less';
import 'plugins/tagcloud/tag_cloud_controller';
import 'plugins/tagcloud/tag_cloud_vis_params';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { VisSchemasProvider } from 'ui/vis/editors/default/schemas';
import tagCloudTemplate from 'plugins/tagcloud/tag_cloud_controller.html';
import { TagCloudVisualization } from './tag_cloud_visualization';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import image from './images/icon-tagcloud.svg';
import { Status } from 'ui/vis/update_status';
VisTypesRegistryProvider.register(function TagCloudProvider(Private) {
VisTypesRegistryProvider.register(function (Private) {
const VisFactory = Private(VisFactoryProvider);
const Schemas = Private(VisSchemasProvider);
return VisFactory.createAngularVisualization({
return VisFactory.createBaseVisualization({
name: 'tagcloud',
title: 'Tag Cloud',
image,
@ -24,11 +24,12 @@ VisTypesRegistryProvider.register(function TagCloudProvider(Private) {
scale: 'linear',
orientation: 'single',
minFontSize: 18,
maxFontSize: 72
},
template: tagCloudTemplate,
maxFontSize: 72,
showLabel: true
}
},
requiresUpdateStatus: [Status.PARAMS, Status.RESIZE],
requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.DATA],
visualization: TagCloudVisualization,
responseHandler: 'tabify',
editorConfig: {
collections: {

View file

@ -40,8 +40,8 @@
</div>
<div>
<label>
<input type="checkbox" value="{{hideLabel}}" ng-model="vis.params.hideLabel" name="hideLabel"
ng-checked="vis.params.hideLabel">
<input type="checkbox" value="{{hideLabel}}" ng-model="vis.params.showLabel" name="showLabel"
ng-checked="vis.params.showLabel">
Show Label
</label>
</div>

View file

@ -0,0 +1,126 @@
import TagCloud from './tag_cloud';
import { Observable } from 'rxjs';
import { render, unmountComponentAtNode } from 'react-dom';
import React from 'react';
import { Label } from './label';
import { FeedbackMessage } from './feedback_message';
const MAX_TAG_COUNT = 200;
export class TagCloudVisualization {
constructor(node, vis) {
this._containerNode = node;
const cloudContainer = document.createElement('div');
cloudContainer.classList.add('tagcloud-vis');
this._containerNode.appendChild(cloudContainer);
this._vis = vis;
this._bucketAgg = null;
this._truncated = false;
this._tagCloud = new TagCloud(cloudContainer);
this._tagCloud.on('select', (event) => {
if (!this._bucketAgg) {
return;
}
const filter = this._bucketAgg.createFilter(event);
this._vis.API.queryFilter.addFilters(filter);
});
this._renderComplete$ = Observable.fromEvent(this._tagCloud, 'renderComplete');
this._feedbackNode = document.createElement('div');
this._containerNode.appendChild(this._feedbackNode);
this._feedbackMessage = render(<FeedbackMessage />, this._feedbackNode);
this._labelNode = document.createElement('div');
this._containerNode.appendChild(this._labelNode);
this._label = render(<Label />, this._labelNode);
}
async render(data, status) {
if (status.params || status.aggs) {
this._updateParams();
}
if (status.data) {
this._updateData(data);
}
if (status.resize) {
this._resize();
}
await this._renderComplete$.take(1).toPromise();
const hasAggDefined = this._vis.aggs[0] && this._vis.aggs[1];
if (!hasAggDefined) {
this._feedbackMessage.setState({
shouldShowTruncate: false,
shouldShowIncomplete: false
});
return;
}
this._label.setState({
label: `${this._vis.aggs[0].makeLabel()} - ${this._vis.aggs[1].makeLabel()}`,
shouldShowLabel: this._vis.params.showLabel
});
this._feedbackMessage.setState({
shouldShowTruncate: this._truncated,
shouldShowIncomplete: this._tagCloud.getStatus() === TagCloud.STATUS.INCOMPLETE
});
}
destroy() {
this._tagCloud.destroy();
unmountComponentAtNode(this._feedbackNode);
unmountComponentAtNode(this._labelNode);
}
_updateData(response) {
if (!response || !response.tables.length) {
this._tagCloud.setData([]);
return;
}
const data = response.tables[0];
this._bucketAgg = this._vis.aggs.find(agg => agg.type.name === 'terms');
const tags = data.rows.map(row => {
const [tag, count] = row;
return {
displayText: this._bucketAgg ? this._bucketAgg.fieldFormatter()(tag) : tag,
rawText: tag,
value: count
};
});
if (tags.length > MAX_TAG_COUNT) {
tags.length = MAX_TAG_COUNT;
this._truncated = true;
} else {
this._truncated = false;
}
this._tagCloud.setData(tags);
}
_updateParams() {
this._tagCloud.setOptions(this._vis.params);
}
_resize() {
this._tagCloud.resize();
}
}