[Lens] Handle failing existence check (#70718)
This commit is contained in:
parent
a0dc26f095
commit
7e533f26aa
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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[]) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue