Skip to content

Commit

Permalink
[Lens] Handle failing existence check (elastic#70718)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Jul 16, 2020
1 parent a0dc26f commit 7e533f2
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 75 deletions.
91 changes: 52 additions & 39 deletions x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export function IndexPatternDataPanel({
id,
title: indexPatterns[id].title,
timeFieldName: indexPatterns[id].timeFieldName,
fields: indexPatterns[id].fields,
}));

const dslQuery = buildSafeEsQuery(
Expand Down Expand Up @@ -197,6 +198,7 @@ export function IndexPatternDataPanel({
charts={charts}
onChangeIndexPattern={onChangeIndexPattern}
existingFields={state.existingFields}
existenceFetchFailed={state.existenceFetchFailed}
/>
)}
</>
Expand Down Expand Up @@ -231,6 +233,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
currentIndexPatternId,
indexPatternRefs,
indexPatterns,
existenceFetchFailed,
query,
dateRange,
filters,
Expand All @@ -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: '',
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +46,7 @@ export interface FieldsAccordionProps {
fieldProps: FieldItemSharedProps;
renderCallout: JSX.Element;
exists: boolean;
showExistenceFetchError?: boolean;
}

export const InnerFieldsAccordion = function InnerFieldsAccordion({
Expand All @@ -58,6 +61,7 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
fieldProps,
renderCallout,
exists,
showExistenceFetchError,
}: FieldsAccordionProps) {
const renderField = useCallback(
(field: IndexPatternField) => {
Expand All @@ -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>
Expand Down
55 changes: 52 additions & 3 deletions x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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(),
Expand All @@ -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 },
Expand All @@ -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(),
Expand All @@ -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,
});
});
});
});
81 changes: 49 additions & 32 deletions x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,49 +246,66 @@ 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;
currentIndexPatternTitle: string;
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,
};

if (pattern.timeFieldName) {
body.timeFieldName = pattern.timeFieldName;
}
const existenceRequests = indexPatterns.map((pattern) => {
const body: Record<string, string | object> = {
dslQuery,
fromDate: dateRange.fromDate,
toDate: dateRange.toDate,
};

return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, {
body: JSON.stringify(body),
}) as Promise<ExistingFields>;
})
);
if (pattern.timeFieldName) {
body.timeFieldName = pattern.timeFieldName;
}

if (isFirstExistenceFetch) {
const fieldsCurrentIndexPattern = emptinessInfo.find(
(info) => info.indexPatternTitle === currentIndexPatternTitle
);
if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) {
showNoDataPopover();
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,
existingFields: emptinessInfo.reduce((acc, info) => {
acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
return acc;
}, state.existingFields),
}));
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[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & {
*/
existingFields: Record<string, Record<string, boolean>>;
isFirstExistenceFetch: boolean;
existenceFetchFailed?: boolean;
};

export interface IndexPatternRef {
Expand Down

0 comments on commit 7e533f2

Please sign in to comment.