Fix TSVB state updates when changing indexpatterns (#24832)

* Move fieldsFetch logic into the vis editor

* Add annotations index pattern change detection

* Fix async update of state. Add functional test

* Add missing data archive

* Force fetch when component mount the first time

* Fix parameters naming

* Refactoring indexPatterns to fetch
This commit is contained in:
Marco Vettorello 2018-11-06 11:50:46 +01:00 committed by GitHub
parent 45f4b1bcf3
commit 89efb81fca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 291 additions and 127 deletions

View file

@ -54,6 +54,7 @@ export const IndexPattern = props => {
disabled={props.disabled}
onChange={handleTextChange(indexPatternName, '*')}
value={model[indexPatternName]}
data-test-subj="metricsIndexPatternInput"
/>
<label className="vis_editor__label" htmlFor={htmlId('timeField')}>
Time Field
@ -67,6 +68,7 @@ export const IndexPattern = props => {
onChange={handleSelectChange(timeFieldName)}
indexPattern={model[indexPatternName]}
fields={fields}
data-test-subj="metricsIndexPatternFieldsSelect"
/>
</div>
<label className="vis_editor__label" htmlFor={htmlId('interval')}>

View file

@ -119,6 +119,7 @@ class MetricPanelConfig extends Component {
aria-selected={selectedTab === 'options'}
className={`kbnTabs__tab${selectedTab === 'options' && '-active' || ''}`}
onClick={() => this.switchTab('options')}
data-test-subj="metricEditorPanelOptionsBtn"
>Panel Options
</button>
</div>

View file

@ -25,6 +25,9 @@ import VisPicker from './vis_picker';
import PanelConfig from './panel_config';
import brushHandler from '../lib/create_brush_handler';
import { get } from 'lodash';
import { extractIndexPatterns } from '../lib/extract_index_patterns';
import { fetchFields } from '../lib/fetch_fields';
import chrome from 'ui/chrome';
class VisEditor extends Component {
constructor(props) {
@ -32,7 +35,13 @@ class VisEditor extends Component {
const { vis } = props;
this.appState = vis.API.getAppState();
const reversed = get(this.appState, 'options.darkTheme', false);
this.state = { model: props.vis.params, dirty: false, autoApply: true, reversed };
this.state = {
model: props.vis.params,
dirty: false,
autoApply: true,
reversed,
visFields: {},
};
this.onBrush = brushHandler(props.vis.API.timeFilter);
this.handleUiState = this.handleUiState.bind(this, props.vis);
this.handleAppStateChange = this.handleAppStateChange.bind(this);
@ -60,27 +69,54 @@ class VisEditor extends Component {
}
}
render() {
const handleChange = (part) => {
const nextModel = { ...this.state.model, ...part };
fetchIndexPatternFields = async () => {
const { params } = this.props.vis;
const { visFields } = this.state;
const indexPatterns = extractIndexPatterns(params, visFields);
const fields = await fetchFields(indexPatterns);
this.setState((previousState) => {
return {
visFields: {
...previousState.visFields,
...fields,
}
};
});
}
this.props.vis.params = nextModel;
if (this.state.autoApply) {
this.props.vis.updateState();
}
setDefaultIndexPattern = async () => {
if (this.props.vis.params.index_pattern === '') {
// set the default index pattern if none is defined.
const savedObjectsClient = chrome.getSavedObjectsClient();
const indexPattern = await savedObjectsClient.get('index-pattern', this.getConfig('defaultIndex'));
const defaultIndexPattern = indexPattern.attributes.title;
this.props.vis.params.index_pattern = defaultIndexPattern;
}
}
this.setState({ model: nextModel, dirty: !this.state.autoApply });
};
const handleAutoApplyToggle = (part) => {
this.setState({ autoApply: part.target.checked });
};
const handleCommit = () => {
handleChange = async (partialModel) => {
const nextModel = { ...this.state.model, ...partialModel };
this.props.vis.params = nextModel;
if (this.state.autoApply) {
this.props.vis.updateState();
this.setState({ dirty: false });
};
}
this.setState({
model: nextModel,
dirty: !this.state.autoApply,
});
this.fetchIndexPatternFields();
}
handleCommit = () => {
this.props.vis.updateState();
this.setState({ dirty: false });
}
handleAutoApplyToggle = (event) => {
this.setState({ autoApply: event.target.checked });
}
render() {
if (!this.props.isEditorMode) {
if (!this.props.vis.params || !this.props.visData) return null;
const reversed = this.state.reversed;
@ -91,7 +127,7 @@ class VisEditor extends Component {
onBrush={this.onBrush}
onUiState={this.handleUiState}
uiState={this.props.vis.getUiState()}
fields={this.props.vis.fields}
fields={this.state.visFields}
model={this.props.vis.params}
visData={this.props.visData}
getConfig={this.getConfig}
@ -105,7 +141,7 @@ class VisEditor extends Component {
return (
<div className="vis_editor">
<div className="vis-editor-hide-for-reporting">
<VisPicker model={model} onChange={handleChange} />
<VisPicker model={model} onChange={this.handleChange} />
</div>
<VisEditorVisualization
dirty={this.state.dirty}
@ -117,20 +153,20 @@ class VisEditor extends Component {
onUiState={this.handleUiState}
uiState={this.props.vis.getUiState()}
onBrush={this.onBrush}
onCommit={handleCommit}
onToggleAutoApply={handleAutoApplyToggle}
onChange={handleChange}
onCommit={this.handleCommit}
onToggleAutoApply={this.handleAutoApplyToggle}
onChange={this.handleChange}
title={this.props.vis.title}
description={this.props.vis.description}
dateFormat={this.props.config.get('dateFormat')}
/>
<div className="vis-editor-hide-for-reporting">
<PanelConfig
fields={this.props.vis.fields}
fields={this.state.visFields}
model={model}
visData={this.props.visData}
dateFormat={this.props.config.get('dateFormat')}
onChange={handleChange}
onChange={this.handleChange}
getConfig={this.getConfig}
/>
</div>
@ -141,7 +177,9 @@ class VisEditor extends Component {
return null;
}
componentDidMount() {
async componentDidMount() {
await this.setDefaultIndexPattern();
await this.fetchIndexPatternFields();
this.props.renderComplete();
}

View file

@ -18,48 +18,27 @@
*/
import React from 'react';
import chrome from 'ui/chrome';
import { render, unmountComponentAtNode } from 'react-dom';
import { FetchFieldsProvider } from '../lib/fetch_fields';
import { extractIndexPatterns } from '../lib/extract_index_patterns';
function ReactEditorControllerProvider(Private, config) {
const fetchFields = Private(FetchFieldsProvider);
const savedObjectsClient = chrome.getSavedObjectsClient();
class ReactEditorController {
constructor(el, savedObj) {
this.el = el;
this.savedObj = savedObj;
this.vis = savedObj.vis;
this.vis.fields = {};
}
render(params) {
return new Promise((resolve) => {
Promise.resolve().then(() => {
if (this.vis.params.index_pattern === '') {
return savedObjectsClient.get('index-pattern', config.get('defaultIndex')).then((indexPattern) => {
this.vis.params.index_pattern = indexPattern.attributes.title;
});
}
}).then(() => {
const indexPatterns = extractIndexPatterns(this.vis);
fetchFields(indexPatterns).then(fields => {
this.vis.fields = { ...fields, ...this.vis.fields };
const Component = this.vis.type.editorConfig.component;
render(<Component
config={config}
vis={this.vis}
savedObj={this.savedObj}
timeRange={params.timeRange}
renderComplete={resolve}
isEditorMode={true}
appState={params.appState}
/>, this.el);
});
});
});
async render(params) {
const Component = this.vis.type.editorConfig.component;
render(<Component
config={config}
vis={this.vis}
savedObj={this.savedObj}
timeRange={params.timeRange}
renderComplete={() => {}}
isEditorMode={true}
appState={params.appState}
/>, this.el);
}
resize() {

View file

@ -22,6 +22,8 @@
color: #c00;
justify-content: center;
padding: 20px;
height: 100%;
width: 100%;
}
.metrics_error__title {

View file

@ -1,3 +1,7 @@
.thor__visualization {
width: 100%;
height: 100%;
}
.thor__input {
padding: 7px 10px;
border-radius: 4px;

View file

@ -20,35 +20,34 @@
import { extractIndexPatterns } from '../extract_index_patterns';
import { expect } from 'chai';
describe('extractIndexPatterns(vis)', () => {
let vis;
let visParams;
let visFields;
beforeEach(() => {
vis = {
fields: {
'*': []
},
params: {
index_pattern: '*',
series: [
{
override_index_pattern: 1,
series_index_pattern: 'example-1-*'
},
{
override_index_pattern: 1,
series_index_pattern: 'example-2-*'
}
],
annotations: [
{ index_pattern: 'notes-*' },
{ index_pattern: 'example-1-*' }
]
}
visFields = {
'*': []
};
visParams = {
index_pattern: '*',
series: [
{
override_index_pattern: 1,
series_index_pattern: 'example-1-*'
},
{
override_index_pattern: 1,
series_index_pattern: 'example-2-*'
}
],
annotations: [
{ index_pattern: 'notes-*' },
{ index_pattern: 'example-1-*' }
]
};
});
it('should return index patterns', () => {
vis.fields = {};
expect(extractIndexPatterns(vis)).to.eql([
visFields = {};
expect(extractIndexPatterns(visParams, visFields)).to.eql([
'*',
'example-1-*',
'example-2-*',
@ -56,8 +55,8 @@ describe('extractIndexPatterns(vis)', () => {
]);
});
it('should return index patterns that do not exist in vis.fields', () => {
expect(extractIndexPatterns(vis)).to.eql([
it('should return index patterns that do not exist in visFields', () => {
expect(extractIndexPatterns(visParams, visFields)).to.eql([
'example-1-*',
'example-2-*',
'notes-*'

View file

@ -18,24 +18,24 @@
*/
import { uniq } from 'lodash';
export function extractIndexPatterns(vis) {
export function extractIndexPatterns(params, fetchedFields) {
const patternsToFetch = [];
if (!vis.fields[vis.params.index_pattern]) {
patternsToFetch.push(vis.params.index_pattern);
if (!fetchedFields[params.index_pattern]) {
patternsToFetch.push(params.index_pattern);
}
vis.params.series.forEach(series => {
params.series.forEach(series => {
const indexPattern = series.series_index_pattern;
if (series.override_index_pattern && !vis.fields[indexPattern]) {
if (series.override_index_pattern && !fetchedFields[indexPattern]) {
patternsToFetch.push(indexPattern);
}
});
if (vis.params.annotations) {
vis.params.annotations.forEach(item => {
if (params.annotations) {
params.annotations.forEach(item => {
const indexPattern = item.index_pattern;
if (indexPattern && !vis.fields[indexPattern]) {
if (indexPattern && !fetchedFields[indexPattern]) {
patternsToFetch.push(indexPattern);
}
});

View file

@ -16,36 +16,34 @@
* specific language governing permissions and limitations
* under the License.
*/
import { kfetch } from 'ui/kfetch';
import { toastNotifications } from 'ui/notify';
const FetchFieldsProvider = (Notifier, $http) => {
const notify = new Notifier({ location: 'Metrics' });
return (indexPatterns = ['*']) => {
if (!Array.isArray(indexPatterns)) indexPatterns = [indexPatterns];
return new Promise((resolve, reject) => {
const fields = {};
Promise.all(indexPatterns.map(pattern => {
const httpResult = $http.get(`../api/metrics/fields?index=${pattern}`)
.then(resp => resp.data)
.catch(resp => { throw resp.data; });
return httpResult
.then(resp => {
if (resp.length && pattern) {
fields[pattern] = resp;
}
})
.catch(resp => {
const err = new Error(resp.message);
err.stack = resp.stack;
notify.error(err);
reject(err);
});
})).then(() => {
resolve(fields);
async function fetchFields(indexPatterns = ['*']) {
const patterns = Array.isArray(indexPatterns) ? indexPatterns : [indexPatterns];
try {
const indexFields = await Promise.all(patterns.map((pattern) => {
return kfetch({
method: 'GET',
pathname: '/api/metrics/fields',
query: {
index: pattern,
}
});
}));
const fields = patterns.reduce((cumulatedFields, currentPattern, index) => {
return {
...cumulatedFields,
[currentPattern]: indexFields[index]
};
}, {});
return fields;
} catch(error) {
toastNotifications.addDanger({
title: 'Unable to load index_pattern fields',
text: error.message,
});
};
};
}
}
export { FetchFieldsProvider };
export { fetchFields };

View file

@ -37,6 +37,7 @@ class Annotation extends Component {
const [ timestamp, messageSource ] = this.props.series;
const reversed = this.props.reversed ? '-reversed' : '';
const messages = messageSource.map((message, i) => {
console.log(message);
return (
<div
key={`${message}-${i}`}

View file

@ -20,6 +20,7 @@
import expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const log = getService('log');
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings', 'visualBuilder']);
@ -208,11 +209,34 @@ export default function ({ getService, getPageObjects }) {
const expectedData = 'OS Count\nwin 8 13\nwin xp 10\nwin 7 12\nios 5\nosx 3';
expect(tableData).to.be(expectedData);
});
});
describe('switch index patterns', () => {
before(async function () {
log.debug('Load kibana_sample_data_flights data');
await esArchiver.loadIfNeeded('kibana_sample_data_flights');
await PageObjects.visualBuilder.resetPage('2015-09-19 06:31:44.000', '2018-10-31 00:0:00.000');
await PageObjects.visualBuilder.clickMetric();
});
after(async function () {
await esArchiver.unload('kibana_sample_data_flights');
});
it('should be able to switch between index patterns', async () => {
const expectedMetricValue = '156';
const value = await PageObjects.visualBuilder.getMetricValue();
log.debug(`metric value: ${value}`);
expect(value).to.eql(expectedMetricValue);
await PageObjects.visualBuilder.clickMetricPanelOptions();
const fromTime = '2018-10-22 00:00:00.000';
const toTime = '2018-10-28 23:59:59.999';
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.visualBuilder.setIndexPatternValue('kibana_sample_data_flights');
await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp');
const newValue = await PageObjects.visualBuilder.getMetricValue();
log.debug(`metric value: ${newValue}`);
expect(newValue).to.eql('10');
});
});
});
}

View file

@ -0,0 +1,100 @@
{
"type": "index",
"value": {
"index": "kibana_sample_data_flights",
"settings": {
"index": {
"number_of_shards": "1",
"number_of_replicas": "0"
}
},
"mappings": {
"_doc": {
"properties": {
"AvgTicketPrice": {
"type": "float"
},
"Cancelled": {
"type": "boolean"
},
"Carrier": {
"type": "keyword"
},
"Dest": {
"type": "keyword"
},
"DestAirportID": {
"type": "keyword"
},
"DestCityName": {
"type": "keyword"
},
"DestCountry": {
"type": "keyword"
},
"DestLocation": {
"type": "geo_point"
},
"DestRegion": {
"type": "keyword"
},
"DestWeather": {
"type": "keyword"
},
"DistanceKilometers": {
"type": "float"
},
"DistanceMiles": {
"type": "float"
},
"FlightDelay": {
"type": "boolean"
},
"FlightDelayMin": {
"type": "integer"
},
"FlightDelayType": {
"type": "keyword"
},
"FlightNum": {
"type": "keyword"
},
"FlightTimeHour": {
"type": "keyword"
},
"FlightTimeMin": {
"type": "float"
},
"Origin": {
"type": "keyword"
},
"OriginAirportID": {
"type": "keyword"
},
"OriginCityName": {
"type": "keyword"
},
"OriginCountry": {
"type": "keyword"
},
"OriginLocation": {
"type": "geo_point"
},
"OriginRegion": {
"type": "keyword"
},
"OriginWeather": {
"type": "keyword"
},
"dayOfWeek": {
"type": "integer"
},
"timestamp": {
"type": "date"
}
}
}
},
"aliases": {}
}
}

View file

@ -193,8 +193,24 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
const tableView = await testSubjects.find('tableView');
return await tableView.getVisibleText();
}
async clickMetricPanelOptions() {
const button = await testSubjects.find('metricEditorPanelOptionsBtn');
await button.click();
await PageObjects.header.waitUntilLoadingHasFinished();
}
async setIndexPatternValue(value) {
const el = await testSubjects.find('metricsIndexPatternInput');
await el.clearValue();
await el.type(value);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async selectIndexPatternTimeField(timeField) {
const el = await testSubjects.find('comboBoxSearchInput');
await el.clearValue();
await el.type(timeField);
await el.session.pressKeys(Keys.RETURN);
await PageObjects.header.waitUntilLoadingHasFinished();
}
}
return new VisualBuilderPage();