[ML] Migrates data visualizer card header to EUI/React (#19890)

To get rid of angular's tooltip="..." code in the header of data cards of the data visualizer, this introduces a ml-field-title-bar directive/component.
- The directive replaces the raw template code in field_data_card.html.
- The directive itself wraps a React component which uses EUI's tooltip instead of angular's tooltip attribute.
- The previous angular template logic (about which classes and fieldnames to display) is also move to the React component
This commit is contained in:
Walter Rafelsberger 2018-06-14 13:16:49 +02:00 committed by GitHub
parent 9d1ec94fac
commit 06bd2d463e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 204 additions and 39 deletions

View file

@ -1,16 +1,5 @@
<div class="ml-field-data-card">
<div class="euiText title-bar" ng-class="card.fieldName===undefined ? 'document_count': card.isUnsupportedType===true ? 'type-other' : card.type">
<ml-field-type-icon
type="card.type"
tooltip-enabled="true"
/>
<div
class="field-name"
tooltip="{{ mlEscape(card.fieldName) || 'document count' }}"
tooltip-append-to-body="true">
{{ card.fieldName || 'document count' }}
</div>
</div>
<ml-field-title-bar card="card" />
<div ng-if="card.loading === true" class="card-contents">
<ml-loading-indicator

View file

@ -11,5 +11,6 @@ import './field_data_card_directive';
import './metric_distribution_chart_directive';
import './top_values_directive';
import './styles/main.less';
import 'plugins/ml/components/field_title_bar';
import 'plugins/ml/components/field_type_icon';
import 'plugins/ml/components/chart_tooltip';

View file

@ -38,30 +38,6 @@
background-color: #bfa180;
}
.title-bar {
color: #ffffff;
font-size: 18px;
text-align: center;
border-radius: 5px 5px 0px 0px;
padding: 5px 6px;
.field-type-icon {
vertical-align: middle;
padding-right: 4px;
display: inline-block;
}
.field-name {
vertical-align: middle;
padding-right: 8px;
max-width: 290px;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.card-contents {
height: 393px;
border-color: #d9d9d9;
@ -197,4 +173,3 @@
}
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { EuiText, EuiToolTip } from '@elastic/eui';
import { FieldTypeIcon } from '../field_type_icon';
export function FieldTitleBar({ card }) {
// don't render and fail gracefully if card prop isn't set
if (typeof card !== 'object' || card === null) {
return null;
}
const classNames = ['ml-field-title-bar'];
if (card.fieldName === undefined) {
classNames.push('document_count');
} else if (card.isUnsupportedType === true) {
classNames.push('type-other');
} else {
classNames.push(card.type);
}
const fieldName = card.fieldName || 'document count';
return (
<EuiText className={classNames.join(' ')}>
<FieldTypeIcon type={card.type} tooltipEnabled={true} />
<EuiToolTip position="left" content={fieldName}>
<div className="field-name">
{fieldName}
</div>
</EuiToolTip>
</EuiText>
);
}
FieldTitleBar.propTypes = {
card: PropTypes.object.isRequired
};

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mount } from 'enzyme';
import React from 'react';
import { FieldTitleBar } from './field_title_bar';
// helper to let PropTypes throw errors instead of just doing console.error()
const error = console.error;
console.error = (warning, ...args) => {
if (/(Invalid prop|Failed prop type)/gi.test(warning)) {
throw new Error(warning);
}
error.apply(console, [warning, ...args]);
};
describe('FieldTitleBar', () => {
test(`throws an error because card is a required prop`, () => {
expect(() => <FieldTitleBar />).toThrow();
});
test(`card prop is an empty object`, () => {
const props = { card: {} };
const wrapper = mount(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual('document count');
const hasClassName = wrapper.find('EuiText').hasClass('document_count');
expect(hasClassName).toBeTruthy();
});
test(`card.isUnsupportedType is true`, () => {
const testFieldName = 'foo';
const props = { card: { fieldName: testFieldName, isUnsupportedType: true } };
const wrapper = mount(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual(testFieldName);
const hasClassName = wrapper.find('EuiText').hasClass('type-other');
expect(hasClassName).toBeTruthy();
});
test(`card.fieldName and card.type is set`, () => {
const testFieldName = 'foo';
const testType = 'bar';
const props = { card: { fieldName: testFieldName, type: testType } };
const wrapper = mount(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual(testFieldName);
const hasClassName = wrapper.find('EuiText').hasClass(testType);
expect(hasClassName).toBeTruthy();
});
test(`tooltip hovering`, () => {
const props = { card: { fieldName: 'foo', type: 'bar' } };
const wrapper = mount(<FieldTitleBar {...props} />);
const container = wrapper.find({ className: 'field-name' });
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
container.simulate('mouseover');
expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
container.simulate('mouseout');
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
});
});

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { FieldTitleBar } from './field_title_bar';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlFieldTitleBar', function () {
return {
restrict: 'E',
replace: false,
scope: {
card: '='
},
link: function (scope, element) {
scope.$watch('card', updateComponent);
updateComponent();
function updateComponent() {
const props = {
card: scope.card
};
ReactDOM.render(
React.createElement(FieldTitleBar, props),
element[0]
);
}
}
};
});

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './field_title_bar_directive';
import './styles/main.less';

View file

@ -0,0 +1,23 @@
.ml-field-title-bar {
color: #ffffff;
font-size: 18px;
text-align: center;
border-radius: 5px 5px 0px 0px;
padding: 5px 6px;
.field-type-icon {
vertical-align: middle;
padding-right: 4px;
display: inline-block;
}
.field-name {
vertical-align: middle;
padding-right: 8px;
max-width: 290px;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}

View file

@ -7,7 +7,7 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
import { FieldTypeIcon } from './field_type_icon_view';
import { FieldTypeIcon } from './field_type_icon';
describe('FieldTypeIcon', () => {

View file

@ -9,7 +9,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { FieldTypeIcon } from './field_type_icon_view.js';
import { FieldTypeIcon } from './field_type_icon.js';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');

View file

@ -8,3 +8,5 @@
import './field_type_icon_directive';
import './styles/main.less';
export { FieldTypeIcon } from './field_type_icon';