mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
45f4b1bcf3
commit
89efb81fca
14 changed files with 291 additions and 127 deletions
|
@ -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')}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
color: #c00;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metrics_error__title {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.thor__visualization {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.thor__input {
|
||||
padding: 7px 10px;
|
||||
border-radius: 4px;
|
||||
|
|
|
@ -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-*'
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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}`}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -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": {}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue