[Lens] Handle failing existence check (#70718)

This commit is contained in:
Joe Reuter 2020-07-16 09:54:46 +02:00 committed by GitHub
parent a0dc26f095
commit 7e533f26aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 76 deletions

View file

@ -125,6 +125,7 @@ export function IndexPatternDataPanel({
id, id,
title: indexPatterns[id].title, title: indexPatterns[id].title,
timeFieldName: indexPatterns[id].timeFieldName, timeFieldName: indexPatterns[id].timeFieldName,
fields: indexPatterns[id].fields,
})); }));
const dslQuery = buildSafeEsQuery( const dslQuery = buildSafeEsQuery(
@ -197,6 +198,7 @@ export function IndexPatternDataPanel({
charts={charts} charts={charts}
onChangeIndexPattern={onChangeIndexPattern} onChangeIndexPattern={onChangeIndexPattern}
existingFields={state.existingFields} existingFields={state.existingFields}
existenceFetchFailed={state.existenceFetchFailed}
/> />
)} )}
</> </>
@ -231,6 +233,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
currentIndexPatternId, currentIndexPatternId,
indexPatternRefs, indexPatternRefs,
indexPatterns, indexPatterns,
existenceFetchFailed,
query, query,
dateRange, dateRange,
filters, filters,
@ -249,6 +252,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
onChangeIndexPattern: (newId: string) => void; onChangeIndexPattern: (newId: string) => void;
existingFields: IndexPatternPrivateState['existingFields']; existingFields: IndexPatternPrivateState['existingFields'];
charts: ChartsPluginSetup; charts: ChartsPluginSetup;
existenceFetchFailed?: boolean;
}) { }) {
const [localState, setLocalState] = useState<DataPanelState>({ const [localState, setLocalState] = useState<DataPanelState>({
nameFilter: '', nameFilter: '',
@ -553,9 +557,15 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
<FieldsAccordion <FieldsAccordion
initialIsOpen={localState.isAvailableAccordionOpen} initialIsOpen={localState.isAvailableAccordionOpen}
id="lnsIndexPatternAvailableFields" id="lnsIndexPatternAvailableFields"
label={i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', { label={
defaultMessage: 'Available fields', existenceFetchFailed
})} ? i18n.translate('xpack.lens.indexPattern.allFieldsLabel', {
defaultMessage: 'All fields',
})
: i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
defaultMessage: 'Available fields',
})
}
exists={true} exists={true}
hasLoaded={!!hasSyncedExistingFields} hasLoaded={!!hasSyncedExistingFields}
fieldsCount={filteredFieldGroups.availableFields.length} fieldsCount={filteredFieldGroups.availableFields.length}
@ -576,6 +586,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength))
); );
}} }}
showExistenceFetchError={existenceFetchFailed}
renderCallout={ renderCallout={
<NoFieldsCallout <NoFieldsCallout
isAffectedByGlobalFilter={!!filters.length} isAffectedByGlobalFilter={!!filters.length}
@ -588,42 +599,44 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
} }
/> />
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<FieldsAccordion {!existenceFetchFailed && (
initialIsOpen={localState.isEmptyAccordionOpen} <FieldsAccordion
isFiltered={ initialIsOpen={localState.isEmptyAccordionOpen}
filteredFieldGroups.emptyFields.length !== fieldGroups.emptyFields.length isFiltered={
} filteredFieldGroups.emptyFields.length !== fieldGroups.emptyFields.length
fieldsCount={filteredFieldGroups.emptyFields.length} }
paginatedFields={paginatedEmptyFields} fieldsCount={filteredFieldGroups.emptyFields.length}
hasLoaded={!!hasSyncedExistingFields} paginatedFields={paginatedEmptyFields}
exists={false} hasLoaded={!!hasSyncedExistingFields}
fieldProps={fieldProps} exists={false}
id="lnsIndexPatternEmptyFields" fieldProps={fieldProps}
label={i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', { id="lnsIndexPatternEmptyFields"
defaultMessage: 'Empty fields', label={i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', {
})} defaultMessage: 'Empty fields',
onToggle={(open) => { })}
setLocalState((s) => ({ onToggle={(open) => {
...s, setLocalState((s) => ({
isEmptyAccordionOpen: open, ...s,
})); isEmptyAccordionOpen: open,
const displayedFieldLength = }));
(localState.isAvailableAccordionOpen const displayedFieldLength =
? filteredFieldGroups.availableFields.length (localState.isAvailableAccordionOpen
: 0) + (open ? filteredFieldGroups.emptyFields.length : 0); ? filteredFieldGroups.availableFields.length
setPageSize( : 0) + (open ? filteredFieldGroups.emptyFields.length : 0);
Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) setPageSize(
); Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength))
}} );
renderCallout={ }}
<NoFieldsCallout renderCallout={
isAffectedByFieldFilter={ <NoFieldsCallout
!!(localState.typeFilter.length || localState.nameFilter.length) isAffectedByFieldFilter={
} !!(localState.typeFilter.length || localState.nameFilter.length)
existFieldsInIndex={!!fieldGroups.emptyFields.length} }
/> existFieldsInIndex={!!fieldGroups.emptyFields.length}
} />
/> }
/>
)}
<EuiSpacer size="m" /> <EuiSpacer size="m" />
</div> </div>
</div> </div>

View file

@ -6,12 +6,14 @@
import './datapanel.scss'; import './datapanel.scss';
import React, { memo, useCallback } from 'react'; import React, { memo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { import {
EuiText, EuiText,
EuiNotificationBadge, EuiNotificationBadge,
EuiSpacer, EuiSpacer,
EuiAccordion, EuiAccordion,
EuiLoadingSpinner, EuiLoadingSpinner,
EuiIconTip,
} from '@elastic/eui'; } from '@elastic/eui';
import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DataPublicPluginStart } from 'src/plugins/data/public';
import { IndexPatternField } from './types'; import { IndexPatternField } from './types';
@ -44,6 +46,7 @@ export interface FieldsAccordionProps {
fieldProps: FieldItemSharedProps; fieldProps: FieldItemSharedProps;
renderCallout: JSX.Element; renderCallout: JSX.Element;
exists: boolean; exists: boolean;
showExistenceFetchError?: boolean;
} }
export const InnerFieldsAccordion = function InnerFieldsAccordion({ export const InnerFieldsAccordion = function InnerFieldsAccordion({
@ -58,6 +61,7 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
fieldProps, fieldProps,
renderCallout, renderCallout,
exists, exists,
showExistenceFetchError,
}: FieldsAccordionProps) { }: FieldsAccordionProps) {
const renderField = useCallback( const renderField = useCallback(
(field: IndexPatternField) => { (field: IndexPatternField) => {
@ -78,7 +82,18 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
</EuiText> </EuiText>
} }
extraAction={ extraAction={
hasLoaded ? ( showExistenceFetchError ? (
<EuiIconTip
aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', {
defaultMessage: 'Existence fetch failed',
})}
type="alert"
color="warning"
content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', {
defaultMessage: "Field information can't be loaded",
})}
/>
) : hasLoaded ? (
<EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}> <EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}>
{fieldsCount} {fieldsCount}
</EuiNotificationBadge> </EuiNotificationBadge>

View file

@ -13,7 +13,7 @@ import {
changeLayerIndexPattern, changeLayerIndexPattern,
syncExistingFields, syncExistingFields,
} from './loader'; } from './loader';
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField } from './types';
import { documentField } from './document_field'; import { documentField } from './document_field';
jest.mock('./operations'); jest.mock('./operations');
@ -642,7 +642,11 @@ describe('loader', () => {
await syncExistingFields({ await syncExistingFields({
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
fetchJson, fetchJson,
indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], indexPatterns: [
{ id: 'a', title: 'a', fields: [] },
{ id: 'b', title: 'a', fields: [] },
{ id: 'c', title: 'a', fields: [] },
],
setState, setState,
dslQuery, dslQuery,
showNoDataPopover: jest.fn(), showNoDataPopover: jest.fn(),
@ -662,6 +666,7 @@ describe('loader', () => {
expect(newState).toEqual({ expect(newState).toEqual({
foo: 'bar', foo: 'bar',
isFirstExistenceFetch: false, isFirstExistenceFetch: false,
existenceFetchFailed: false,
existingFields: { existingFields: {
a: { a_field_1: true, a_field_2: true }, a: { a_field_1: true, a_field_2: true },
b: { b_field_1: true, b_field_2: true }, b: { b_field_1: true, b_field_2: true },
@ -687,7 +692,11 @@ describe('loader', () => {
const args = { const args = {
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
fetchJson, fetchJson,
indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], indexPatterns: [
{ id: 'a', title: 'a', fields: [] },
{ id: 'b', title: 'a', fields: [] },
{ id: 'c', title: 'a', fields: [] },
],
setState, setState,
dslQuery, dslQuery,
showNoDataPopover: jest.fn(), showNoDataPopover: jest.fn(),
@ -702,5 +711,45 @@ describe('loader', () => {
await syncExistingFields({ ...args, isFirstExistenceFetch: true }); await syncExistingFields({ ...args, isFirstExistenceFetch: true });
expect(showNoDataPopover).not.toHaveBeenCalled(); expect(showNoDataPopover).not.toHaveBeenCalled();
}); });
it('should set all fields to available and existence error flag if the request fails', async () => {
const setState = jest.fn();
const fetchJson = (jest.fn((path: string) => {
return new Promise((resolve, reject) => {
reject(new Error());
});
}) as unknown) as HttpHandler;
const args = {
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
fetchJson,
indexPatterns: [
{
id: 'a',
title: 'a',
fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[],
},
],
setState,
dslQuery,
showNoDataPopover: jest.fn(),
currentIndexPatternTitle: 'abc',
isFirstExistenceFetch: false,
};
await syncExistingFields(args);
const [fn] = setState.mock.calls[0];
const newState = fn({
foo: 'bar',
existingFields: {},
}) as IndexPatternPrivateState;
expect(newState.existenceFetchFailed).toEqual(true);
expect(newState.existingFields.a).toEqual({
field1: true,
field2: true,
});
});
}); });
}); });

View file

@ -246,7 +246,12 @@ export async function syncExistingFields({
showNoDataPopover, showNoDataPopover,
}: { }: {
dateRange: DateRange; dateRange: DateRange;
indexPatterns: Array<{ id: string; timeFieldName?: string | null }>; indexPatterns: Array<{
id: string;
title: string;
fields: IndexPatternField[];
timeFieldName?: string | null;
}>;
fetchJson: HttpSetup['post']; fetchJson: HttpSetup['post'];
setState: SetState; setState: SetState;
isFirstExistenceFetch: boolean; isFirstExistenceFetch: boolean;
@ -254,41 +259,53 @@ export async function syncExistingFields({
dslQuery: object; dslQuery: object;
showNoDataPopover: () => void; showNoDataPopover: () => void;
}) { }) {
const emptinessInfo = await Promise.all( const existenceRequests = indexPatterns.map((pattern) => {
indexPatterns.map((pattern) => { const body: Record<string, string | object> = {
const body: Record<string, string | object> = { dslQuery,
dslQuery, fromDate: dateRange.fromDate,
fromDate: dateRange.fromDate, toDate: dateRange.toDate,
toDate: dateRange.toDate, };
};
if (pattern.timeFieldName) { if (pattern.timeFieldName) {
body.timeFieldName = pattern.timeFieldName; body.timeFieldName = pattern.timeFieldName;
}
return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, {
body: JSON.stringify(body),
}) as Promise<ExistingFields>;
})
);
if (isFirstExistenceFetch) {
const fieldsCurrentIndexPattern = emptinessInfo.find(
(info) => info.indexPatternTitle === currentIndexPatternTitle
);
if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) {
showNoDataPopover();
} }
}
setState((state) => ({ return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, {
...state, body: JSON.stringify(body),
isFirstExistenceFetch: false, }) as Promise<ExistingFields>;
existingFields: emptinessInfo.reduce((acc, info) => { });
acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
return acc; try {
}, state.existingFields), const emptinessInfo = await Promise.all(existenceRequests);
})); if (isFirstExistenceFetch) {
const fieldsCurrentIndexPattern = emptinessInfo.find(
(info) => info.indexPatternTitle === currentIndexPatternTitle
);
if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) {
showNoDataPopover();
}
}
setState((state) => ({
...state,
isFirstExistenceFetch: false,
existenceFetchFailed: false,
existingFields: emptinessInfo.reduce((acc, info) => {
acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
return acc;
}, state.existingFields),
}));
} catch (e) {
// show all fields as available if fetch failed
setState((state) => ({
...state,
existenceFetchFailed: true,
existingFields: indexPatterns.reduce((acc, pattern) => {
acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name));
return acc;
}, state.existingFields),
}));
}
} }
function booleanMap(keys: string[]) { function booleanMap(keys: string[]) {

View file

@ -52,6 +52,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & {
*/ */
existingFields: Record<string, Record<string, boolean>>; existingFields: Record<string, Record<string, boolean>>;
isFirstExistenceFetch: boolean; isFirstExistenceFetch: boolean;
existenceFetchFailed?: boolean;
}; };
export interface IndexPatternRef { export interface IndexPatternRef {