[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:
parent
9d1ec94fac
commit
06bd2d463e
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
10
x-pack/plugins/ml/public/components/field_title_bar/index.js
Normal file
10
x-pack/plugins/ml/public/components/field_title_bar/index.js
Normal 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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
@ -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');
|
||||
|
|
|
@ -8,3 +8,5 @@
|
|||
|
||||
import './field_type_icon_directive';
|
||||
import './styles/main.less';
|
||||
|
||||
export { FieldTypeIcon } from './field_type_icon';
|
||||
|
|
Loading…
Reference in a new issue