mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
c7f63f8f8d
commit
f82a575a54
6 changed files with 161 additions and 19 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
34
x-pack/plugins/graph/public/components/tooltip_wrapper.tsx
Normal file
34
x-pack/plugins/graph/public/components/tooltip_wrapper.tsx
Normal 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
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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()
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue