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

View file

@ -6,12 +6,14 @@
import './datapanel.scss';
import React, { memo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiText,
EuiNotificationBadge,
EuiSpacer,
EuiAccordion,
EuiLoadingSpinner,
EuiIconTip,
} from '@elastic/eui';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { IndexPatternField } from './types';
@ -44,6 +46,7 @@ export interface FieldsAccordionProps {
fieldProps: FieldItemSharedProps;
renderCallout: JSX.Element;
exists: boolean;
showExistenceFetchError?: boolean;
}
export const InnerFieldsAccordion = function InnerFieldsAccordion({
@ -58,6 +61,7 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
fieldProps,
renderCallout,
exists,
showExistenceFetchError,
}: FieldsAccordionProps) {
const renderField = useCallback(
(field: IndexPatternField) => {
@ -78,7 +82,18 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
</EuiText>
}
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'}>
{fieldsCount}
</EuiNotificationBadge>

View file

@ -13,7 +13,7 @@ import {
changeLayerIndexPattern,
syncExistingFields,
} from './loader';
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField } from './types';
import { documentField } from './document_field';
jest.mock('./operations');
@ -642,7 +642,11 @@ describe('loader', () => {
await syncExistingFields({
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
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,
dslQuery,
showNoDataPopover: jest.fn(),
@ -662,6 +666,7 @@ describe('loader', () => {
expect(newState).toEqual({
foo: 'bar',
isFirstExistenceFetch: false,
existenceFetchFailed: false,
existingFields: {
a: { a_field_1: true, a_field_2: true },
b: { b_field_1: true, b_field_2: true },
@ -687,7 +692,11 @@ describe('loader', () => {
const args = {
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
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,
dslQuery,
showNoDataPopover: jest.fn(),
@ -702,5 +711,45 @@ describe('loader', () => {
await syncExistingFields({ ...args, isFirstExistenceFetch: true });
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,
}: {
dateRange: DateRange;
indexPatterns: Array<{ id: string; timeFieldName?: string | null }>;
indexPatterns: Array<{
id: string;
title: string;
fields: IndexPatternField[];
timeFieldName?: string | null;
}>;
fetchJson: HttpSetup['post'];
setState: SetState;
isFirstExistenceFetch: boolean;
@ -254,41 +259,53 @@ export async function syncExistingFields({
dslQuery: object;
showNoDataPopover: () => void;
}) {
const emptinessInfo = await Promise.all(
indexPatterns.map((pattern) => {
const body: Record<string, string | object> = {
dslQuery,
fromDate: dateRange.fromDate,
toDate: dateRange.toDate,
};
const existenceRequests = indexPatterns.map((pattern) => {
const body: Record<string, string | object> = {
dslQuery,
fromDate: dateRange.fromDate,
toDate: dateRange.toDate,
};
if (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();
if (pattern.timeFieldName) {
body.timeFieldName = pattern.timeFieldName;
}
}
setState((state) => ({
...state,
isFirstExistenceFetch: false,
existingFields: emptinessInfo.reduce((acc, info) => {
acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
return acc;
}, state.existingFields),
}));
return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, {
body: JSON.stringify(body),
}) as Promise<ExistingFields>;
});
try {
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[]) {

View file

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