[Graph] Make graph app resilient to no fields or missing data views (#126441)

* 🐛 Fix broken scenarios with no fields or dataviews

* Update x-pack/plugins/graph/public/state_management/persistence.ts

* Update x-pack/plugins/graph/public/components/search_bar.tsx

Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>
This commit is contained in:
Marco Liberati 2022-03-01 11:13:28 +01:00 committed by GitHub
parent c7f63f8f8d
commit f82a575a54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 161 additions and 19 deletions

View file

@ -6,7 +6,7 @@
*/
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { SearchBar, SearchBarProps } from './search_bar';
import { SearchBar, SearchBarProps, SearchBarComponent, SearchBarStateProps } from './search_bar';
import React, { Component, ReactElement } from 'react';
import { CoreStart } from 'src/core/public';
import { act } from 'react-dom/test-utils';
@ -26,8 +26,8 @@ jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() }));
const waitForIndexPatternFetch = () => new Promise((r) => setTimeout(r));
function wrapSearchBarInContext(testProps: SearchBarProps) {
const services = {
function getServiceMocks() {
return {
uiSettings: {
get: (key: string) => {
return 10;
@ -56,7 +56,10 @@ function wrapSearchBarInContext(testProps: SearchBarProps) {
},
},
};
}
function wrapSearchBarInContext(testProps: SearchBarProps) {
const services = getServiceMocks();
return (
<I18nProvider>
<KibanaContextProvider services={services}>
@ -120,6 +123,21 @@ describe('search_bar', () => {
});
}
async function mountSearchBarWithExplicitContext(props: SearchBarProps & SearchBarStateProps) {
jest.clearAllMocks();
const services = getServiceMocks();
await act(async () => {
instance = mountWithIntl(
<I18nProvider>
<KibanaContextProvider services={services}>
<SearchBarComponent {...props} />
</KibanaContextProvider>
</I18nProvider>
);
});
}
it('should render search bar and fetch index pattern', async () => {
await mountSearchBar();
@ -175,4 +193,44 @@ describe('search_bar', () => {
expect(openSourceModal).toHaveBeenCalled();
});
it('should disable the graph button when no data view is configured', async () => {
const stateProps = {
submit: jest.fn(),
onIndexPatternSelected: jest.fn(),
currentDatasource: undefined,
selectedFields: [],
};
await mountSearchBarWithExplicitContext({
urlQuery: null,
...defaultProps,
...stateProps,
});
expect(
instance.find('[data-test-subj="graph-explore-button"]').first().prop('disabled')
).toBeTruthy();
});
it('should disable the graph button when no field is configured', async () => {
const stateProps = {
submit: jest.fn(),
onIndexPatternSelected: jest.fn(),
currentDatasource: {
type: 'indexpattern' as const,
id: '123',
title: 'test-index',
},
selectedFields: [],
};
await mountSearchBarWithExplicitContext({
urlQuery: null,
...defaultProps,
...stateProps,
});
expect(
instance.find('[data-test-subj="graph-explore-button"]').first().prop('disabled')
).toBeTruthy();
});
});

View file

@ -10,7 +10,7 @@ import React, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { connect } from 'react-redux';
import { IndexPatternSavedObject, IndexPatternProvider } from '../types';
import { IndexPatternSavedObject, IndexPatternProvider, WorkspaceField } from '../types';
import { openSourceModal } from '../services/source_modal';
import {
GraphState,
@ -18,6 +18,7 @@ import {
requestDatasource,
IndexpatternDatasource,
submitSearch,
selectedFieldsSelector,
} from '../state_management';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
@ -28,6 +29,7 @@ import {
Query,
esKuery,
} from '../../../../../src/plugins/data/public';
import { TooltipWrapper } from './tooltip_wrapper';
export interface SearchBarProps {
isLoading: boolean;
@ -44,6 +46,7 @@ export interface SearchBarProps {
export interface SearchBarStateProps {
currentDatasource?: IndexpatternDatasource;
selectedFields: WorkspaceField[];
onIndexPatternSelected: (indexPattern: IndexPatternSavedObject) => void;
submit: (searchTerm: string) => void;
}
@ -74,6 +77,7 @@ export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps)
currentIndexPattern,
currentDatasource,
indexPatternProvider,
selectedFields,
submit,
onIndexPatternSelected,
confirmWipeWorkspace,
@ -170,14 +174,27 @@ export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps)
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
type="submit"
disabled={isLoading || !currentIndexPattern}
data-test-subj="graph-explore-button"
<TooltipWrapper
condition={!currentIndexPattern || !selectedFields.length}
tooltipContent={
!currentIndexPattern
? i18n.translate('xpack.graph.bar.exploreLabelNoIndexPattern', {
defaultMessage: 'Select a data source',
})
: i18n.translate('xpack.graph.bar.exploreLabelNoFields', {
defaultMessage: 'Select at least one field',
})
}
>
{i18n.translate('xpack.graph.bar.exploreLabel', { defaultMessage: 'Graph' })}
</EuiButton>
<EuiButton
fill
type="submit"
disabled={isLoading || !currentIndexPattern || !selectedFields.length}
data-test-subj="graph-explore-button"
>
{i18n.translate('xpack.graph.bar.exploreLabel', { defaultMessage: 'Graph' })}
</EuiButton>
</TooltipWrapper>
</EuiFlexItem>
</EuiFlexGroup>
</form>
@ -190,6 +207,7 @@ export const SearchBar = connect(
return {
currentDatasource:
datasource.current.type === 'indexpattern' ? datasource.current : undefined,
selectedFields: selectedFieldsSelector(state),
};
},
(dispatch) => ({

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiToolTip, EuiToolTipProps } from '@elastic/eui';
export type TooltipWrapperProps = Partial<Omit<EuiToolTipProps, 'content'>> & {
tooltipContent: string;
/** When the condition is truthy, the tooltip will be shown */
condition: boolean;
};
export const TooltipWrapper: React.FunctionComponent<TooltipWrapperProps> = ({
children,
condition,
tooltipContent,
...tooltipProps
}) => {
return (
<>
{condition ? (
<EuiToolTip content={tooltipContent} delay="long" {...tooltipProps}>
<>{children}</>
</EuiToolTip>
) : (
children
)}
</>
);
};

View file

@ -59,9 +59,12 @@ export function createMockGraphStore({
createWorkspace: jest.fn((index, advancedSettings) => workspaceMock),
getWorkspace: jest.fn(() => workspaceMock),
indexPatternProvider: {
get: jest.fn(() =>
Promise.resolve({ id: '123', title: 'test-pattern' } as unknown as IndexPattern)
),
get: jest.fn(async (id: string) => {
if (id === 'missing-dataview') {
throw Error('No data view with this id');
}
return { id: '123', title: 'test-pattern' } as unknown as IndexPattern;
}),
},
I18nContext: jest
.fn()

View file

@ -18,7 +18,11 @@ import { IndexpatternDatasource, datasourceSelector } from './datasource';
import { fieldsSelector } from './fields';
import { metaDataSelector, updateMetaData } from './meta_data';
import { templatesSelector } from './url_templates';
import { migrateLegacyIndexPatternRef, appStateToSavedWorkspace } from '../services/persistence';
import {
migrateLegacyIndexPatternRef,
appStateToSavedWorkspace,
lookupIndexPatternId,
} from '../services/persistence';
import { settingsSelector } from './advanced_settings';
import { openSaveModal } from '../services/save_modal';
@ -96,6 +100,21 @@ describe('persistence sagas', () => {
const resultingState = env.store.getState();
expect(datasourceSelector(resultingState).current.type).toEqual('none');
});
it('should not crash if the data view goes missing', async () => {
(lookupIndexPatternId as jest.Mock).mockReturnValueOnce('missing-dataview');
env.store.dispatch(
loadSavedWorkspace({
savedWorkspace: {
title: 'my workspace',
},
} as LoadSavedWorkspacePayload)
);
await waitForPromise();
expect(env.mockedDeps.notifications.toasts.addDanger).toHaveBeenCalledWith(
'Data view "missing-dataview" not found'
);
});
});
describe('saving saga', () => {

View file

@ -66,10 +66,20 @@ export const loadingSaga = ({
}
const selectedIndexPatternId = lookupIndexPatternId(savedWorkspace);
const indexPattern = (yield call(
indexPatternProvider.get,
selectedIndexPatternId
)) as IndexPattern;
let indexPattern;
try {
indexPattern = (yield call(indexPatternProvider.get, selectedIndexPatternId)) as IndexPattern;
} catch (e) {
notifications.toasts.addDanger(
i18n.translate('xpack.graph.loadWorkspace.missingDataViewErrorMessage', {
defaultMessage: 'Data view "{name}" not found',
values: {
name: selectedIndexPatternId,
},
})
);
return;
}
const initialSettings = settingsSelector((yield select()) as GraphState);
const createdWorkspace = createWorkspace(indexPattern.title, initialSettings);