mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
- Implements tag_cloud as a Base Visualization - Use React for labels - Introduce screenshot comparison unit tests
This commit is contained in:
parent
af8a04c5f1
commit
46641f942e
11 changed files with 333 additions and 100 deletions
BIN
src/core_plugins/tagcloud/public/__tests__/afterparamchange.png
Normal file
BIN
src/core_plugins/tagcloud/public/__tests__/afterparamchange.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
src/core_plugins/tagcloud/public/__tests__/afterresize.png
Normal file
BIN
src/core_plugins/tagcloud/public/__tests__/afterresize.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9 KiB |
BIN
src/core_plugins/tagcloud/public/__tests__/basicdraw.png
Normal file
BIN
src/core_plugins/tagcloud/public/__tests__/basicdraw.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
|
22
src/core_plugins/tagcloud/public/feedback_message.js
Normal file
22
src/core_plugins/tagcloud/public/feedback_message.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
18
src/core_plugins/tagcloud/public/label.js
Normal file
18
src/core_plugins/tagcloud/public/label.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
126
src/core_plugins/tagcloud/public/tag_cloud_visualization.js
Normal file
126
src/core_plugins/tagcloud/public/tag_cloud_visualization.js
Normal 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();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue