[Lens] Handle failing existence check (#70718)
This commit is contained in:
parent
a0dc26f095
commit
7e533f26aa
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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[]) {
|
||||
|
|
|
@ -52,6 +52,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & {
|
|||
*/
|
||||
existingFields: Record<string, Record<string, boolean>>;
|
||||
isFirstExistenceFetch: boolean;
|
||||
existenceFetchFailed?: boolean;
|
||||
};
|
||||
|
||||
export interface IndexPatternRef {
|
||||
|
|
Loading…
Reference in a new issue