Fixes index pattern wizard when there are remote clusters but no local indices (#24339)

* don't hide wizard if clusters exist

* catch errors

* add toast notifs if unable to load data
This commit is contained in:
Matthew Kime 2018-11-13 14:00:27 -06:00 committed by GitHub
parent ca1d280818
commit bb250560ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 406 additions and 122 deletions

View file

@ -30,6 +30,7 @@ import { managementApi } from './server/routes/api/management';
import { scriptsApi } from './server/routes/api/scripts';
import { registerSuggestionsApi } from './server/routes/api/suggestions';
import { registerKqlTelemetryApi } from './server/routes/api/kql_telemetry';
import { registerClustersRoute } from './server/routes/api/remote_info';
import { registerFieldFormats } from './server/field_formats/register';
import { registerTutorials } from './server/tutorials/register';
import * as systemApi from './server/lib/system_api';
@ -167,6 +168,7 @@ export default function (kibana) {
registerFieldFormats(server);
registerTutorials(server);
makeKQLUsageCollector(server);
registerClustersRoute(server);
server.expose('systemApi', systemApi);
server.expose('handleEsError', handleEsError);
server.injectUiAppVars('kibana', () => injectVars(server));

View file

@ -1,132 +1,205 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
<React.Fragment>
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<LoadingState />
</div>
<EuiGlobalToastList
dismissToast={[Function]}
toastLifeTimeMs={6000}
toasts={Array []}
/>
<LoadingState />
</div>
</React.Fragment>
`;
exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = `
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepIndexPattern
allIndices={
Array [
Object {
"name": "myIndexPattern",
},
]
}
esService={Object {}}
goToNextStep={[Function]}
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getIsBeta": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
<React.Fragment>
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepIndexPattern
allIndices={
Array [
Object {
"name": "myIndexPattern",
},
]
}
}
initialQuery=""
isIncludingSystemIndices={false}
savedObjectsClient={Object {}}
esService={Object {}}
goToNextStep={[Function]}
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getIsBeta": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
}
}
initialQuery=""
isIncludingSystemIndices={false}
savedObjectsClient={Object {}}
/>
</div>
<EuiGlobalToastList
dismissToast={[Function]}
toastLifeTimeMs={6000}
toasts={Array []}
/>
</div>
</React.Fragment>
`;
exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = `
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
<React.Fragment>
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<EmptyState
onRefresh={[Function]}
/>
</div>
<EuiGlobalToastList
dismissToast={[Function]}
toastLifeTimeMs={6000}
toasts={Array []}
/>
<EmptyState
onRefresh={[Function]}
/>
</div>
</React.Fragment>
`;
exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = `
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepTimeField
createIndexPattern={[Function]}
goToPreviousStep={[Function]}
indexPattern=""
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getIsBeta": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
<React.Fragment>
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepTimeField
createIndexPattern={[Function]}
goToPreviousStep={[Function]}
indexPattern=""
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getIsBeta": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
}
}
}
indexPatternsService={Object {}}
indexPatternsService={Object {}}
/>
</div>
<EuiGlobalToastList
dismissToast={[Function]}
toastLifeTimeMs={6000}
toasts={Array []}
/>
</div>
</React.Fragment>
`;
exports[`CreateIndexPatternWizard renders when there are no indices but there are remote clusters 1`] = `
<React.Fragment>
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepIndexPattern
allIndices={Array []}
esService={Object {}}
goToNextStep={[Function]}
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getIsBeta": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
}
}
initialQuery=""
isIncludingSystemIndices={false}
savedObjectsClient={Object {}}
/>
</div>
<EuiGlobalToastList
dismissToast={[Function]}
toastLifeTimeMs={6000}
toasts={Array []}
/>
</React.Fragment>
`;
exports[`CreateIndexPatternWizard shows system indices even if there are no other indices if the include system indices is toggled 1`] = `
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={true}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepIndexPattern
allIndices={
Array [
Object {
"name": ".kibana ",
},
]
}
esService={Object {}}
goToNextStep={[Function]}
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getIsBeta": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
<React.Fragment>
<div>
<Header
indexPatternName="name"
isBeta={false}
isIncludingSystemIndices={true}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepIndexPattern
allIndices={
Array [
Object {
"name": ".kibana ",
},
]
}
}
initialQuery=""
isIncludingSystemIndices={true}
savedObjectsClient={Object {}}
esService={Object {}}
goToNextStep={[Function]}
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getIsBeta": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
}
}
initialQuery=""
isIncludingSystemIndices={true}
savedObjectsClient={Object {}}
/>
</div>
<EuiGlobalToastList
dismissToast={[Function]}
toastLifeTimeMs={6000}
toasts={Array []}
/>
</div>
</React.Fragment>
`;

View file

@ -42,6 +42,9 @@ jest.mock('../lib/get_indices', () => ({
];
},
}));
jest.mock('ui/chrome', () => ({
addBasePath: () => { },
}));
const loadingDataDocUrl = '';
const initialQuery = '';
@ -80,6 +83,26 @@ describe('CreateIndexPatternWizard', () => {
component.setState({
isInitiallyLoadingIndices: false,
allIndices: [],
remoteClustersExist: false
});
await component.update();
expect(component).toMatchSnapshot();
});
it('renders when there are no indices but there are remote clusters', async () => {
const component = shallow(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
initialQuery={initialQuery}
services={services}
/>
);
component.setState({
isInitiallyLoadingIndices: false,
allIndices: [],
remoteClustersExist: true
});
await component.update();

View file

@ -32,6 +32,7 @@ jest.mock('ui/chrome', () => ({
getUiSettingsClient: () => ({
get: () => '',
}),
addBasePath: () => { },
}));
const { renderCreateIndexPatternWizard, destroyCreateIndexPatternWizard } = require('../render');

View file

@ -41,6 +41,7 @@ jest.mock('ui/chrome', () => ({
getUiSettingsClient: () => ({
get: () => '',
}),
addBasePath: () => { },
}));
jest.mock('../../../lib/get_indices', () => ({
@ -62,7 +63,7 @@ const esService = {};
const savedObjectsClient = {
find: () => ({ savedObjects: [] })
};
const goToNextStep = () => {};
const goToNextStep = () => { };
const createComponent = props => {
return shallowWithIntl(

View file

@ -29,6 +29,9 @@ jest.mock('../components/action_buttons', () => ({ ActionButtons: 'ActionButtons
jest.mock('../../../lib/extract_time_fields', () => ({
extractTimeFields: fields => fields,
}));
jest.mock('ui/chrome', () => ({
addBasePath: () => { },
}));
const mockIndexPatternCreationType = {
getIndexPatternType: () => 'default',

View file

@ -20,6 +20,11 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
EuiGlobalToastList
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { StepIndexPattern } from './components/step_index_pattern';
import { StepTimeField } from './components/step_time_field';
import { Header } from './components/header';
@ -30,6 +35,7 @@ import { MAX_SEARCH_SIZE } from './constants';
import {
ensureMinimumTime,
getIndices,
getRemoteClusters
} from './lib';
export class CreateIndexPatternWizard extends Component {
@ -52,20 +58,62 @@ export class CreateIndexPatternWizard extends Component {
step: 1,
indexPattern: '',
allIndices: [],
remoteClustersExist: false,
isInitiallyLoadingIndices: true,
isIncludingSystemIndices: false,
toasts: []
};
}
async componentWillMount() {
this.fetchIndices();
this.fetchData();
}
fetchIndices = async () => {
this.setState({ allIndices: [], isInitiallyLoadingIndices: true });
catchAndWarn = async (asyncFn, errorValue, errorMsg) => {
try {
return await asyncFn;
} catch (errors) {
this.setState(prevState => ({
toasts: prevState.toasts.concat([{
title: errorMsg,
id: errorMsg,
color: 'warning',
iconType: 'alert',
}])
}));
return errorValue;
}
};
fetchData = async () => {
const { services } = this.props;
const allIndices = await ensureMinimumTime(getIndices(services.es, this.indexPatternCreationType, `*`, MAX_SEARCH_SIZE)); //
this.setState({ allIndices, isInitiallyLoadingIndices: false });
this.setState({
allIndices: [],
isInitiallyLoadingIndices: true,
remoteClustersExist: false
});
const indicesFailMsg = (<FormattedMessage
id="kbn.management.createIndexPattern.loadIndicesFailMsg"
defaultMessage="Failed to load indices"
/>);
const clustersFailMsg = (<FormattedMessage
id="kbn.management.createIndexPattern.loadClustersFailMsg"
defaultMessage="Failed to load remote clusters"
/>);
const [allIndices, remoteClusters] = await ensureMinimumTime([
this.catchAndWarn(getIndices(services.es, this.indexPatternCreationType, `*`, MAX_SEARCH_SIZE), [], indicesFailMsg),
this.catchAndWarn(getRemoteClusters(services.$http), [], clustersFailMsg)
]);
this.setState({
allIndices,
isInitiallyLoadingIndices: false,
remoteClustersExist: remoteClusters.length !== 0
});
}
createIndexPattern = async (timeFieldName, indexPatternId) => {
@ -127,6 +175,7 @@ export class CreateIndexPatternWizard extends Component {
isIncludingSystemIndices,
step,
indexPattern,
remoteClustersExist
} = this.state;
if (isInitiallyLoadingIndices) {
@ -134,8 +183,10 @@ export class CreateIndexPatternWizard extends Component {
}
const hasDataIndices = allIndices.some(({ name }) => !name.startsWith('.'));
if (!hasDataIndices && !isIncludingSystemIndices) {
return <EmptyState onRefresh={this.fetchIndices} />;
if (!hasDataIndices &&
!isIncludingSystemIndices &&
!remoteClustersExist) {
return <EmptyState onRefresh={this.fetchData} />;
}
if (step === 1) {
@ -169,15 +220,28 @@ export class CreateIndexPatternWizard extends Component {
return null;
}
removeToast = (removedToast) => {
this.setState(prevState => ({
toasts: prevState.toasts.filter(toast => toast.id !== removedToast.id),
}));
};
render() {
const header = this.renderHeader();
const content = this.renderContent();
return (
<div>
{header}
{content}
</div>
<React.Fragment>
<div>
{header}
{content}
</div>
<EuiGlobalToastList
toasts={this.state.toasts}
dismissToast={this.removeToast}
toastLifeTimeMs={6000}
/>
</React.Fragment>
);
}
}

View file

@ -39,6 +39,7 @@ uiRoutes.when('/management/kibana/index', {
config: $injector.get('config'),
es: $injector.get('es'),
indexPatterns: $injector.get('indexPatterns'),
$http: $injector.get('$http'),
savedObjectsClient: Private(SavedObjectsClientProvider),
indexPatternCreationType,
changeUrl: url => {

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import chrome from 'ui/chrome';
const apiPrefix = chrome.addBasePath('/api/kibana');
export async function getRemoteClusters($http) {
const response = await $http.get(`${apiPrefix}/clusters`);
return response.data;
}

View file

@ -28,3 +28,5 @@ export { getMatchedIndices } from './get_matched_indices';
export { containsIllegalCharacters } from './contains_illegal_characters';
export { extractTimeFields } from './extract_time_fields';
export { getRemoteClusters } from './get_remote_clusters';

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* 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 { once } from 'lodash';
const callWithRequest = once(server => {
const cluster = server.plugins.elasticsearch.getCluster('data');
return cluster.callWithRequest;
});
export const callWithRequestFactory = (server, request) => {
return (...args) => {
return callWithRequest(server)(request, ...args);
};
};

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* 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 { callWithRequestFactory } from './call_with_request_factory';
import handleEsError from '../../../lib/handle_es_error';
async function fetchRemoteClusters(callWithRequest) {
const options = {
method: 'GET',
path: '_remote/info'
};
const remoteInfo = await callWithRequest('transport.request', options);
return Object.keys(remoteInfo);
}
export function registerClustersRoute(server) {
server.route({
path: '/api/kibana/clusters',
method: 'GET',
handler: async request => {
const callWithRequest = callWithRequestFactory(server, request);
try {
return await fetchRemoteClusters(callWithRequest);
} catch (error) {
throw handleEsError(error);
}
}
});
}

View file

@ -7,7 +7,7 @@
import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers';
import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
import { fetchAliases } from './fetch_aliases';
function formatHits(hits, aliases) {
@ -60,7 +60,7 @@ export function registerListRoute(server) {
}
},
config: {
pre: [ licensePreRouting ]
pre: [licensePreRouting]
}
});
}